prysm-pulse/validator/keymanager/remote/keymanager.go
Håvard Anda Estensen d2f4a8cc7c
Replace ioutil with io and os (#10541)
* Replace ioutil with io and os

* Fix build errors
2022-04-18 20:42:07 +00:00

315 lines
10 KiB
Go

package remote
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/golang/protobuf/ptypes/empty"
"github.com/logrusorgru/aurora"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/async/event"
fieldparams "github.com/prysmaticlabs/prysm/config/fieldparams"
"github.com/prysmaticlabs/prysm/crypto/bls"
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
ethpbservice "github.com/prysmaticlabs/prysm/proto/eth/service"
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/validator/keymanager"
remote_utils "github.com/prysmaticlabs/prysm/validator/keymanager/remote-utils"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
var (
// ErrSigningFailed defines a failure from the remote server
// when performing a signing operation.
ErrSigningFailed = errors.New("signing failed in the remote server")
// ErrSigningDenied defines a failure from the remote server when
// performing a signing operation was denied by a remote server.
ErrSigningDenied = errors.New("signing request was denied by remote server")
)
// RemoteKeymanager defines the interface for remote Prysm wallets.
type RemoteKeymanager interface {
keymanager.IKeymanager
ReloadPublicKeys(ctx context.Context) ([][fieldparams.BLSPubkeyLength]byte, error)
}
// KeymanagerOpts for a remote keymanager.
type KeymanagerOpts struct {
RemoteCertificate *CertificateConfig `json:"remote_cert"`
RemoteAddr string `json:"remote_address"`
}
// CertificateConfig defines configuration options for
// certificate authority certs, client certs, and client keys
// for TLS gRPC connections.
type CertificateConfig struct {
RequireTls bool `json:"require_tls"`
ClientCertPath string `json:"crt_path"`
ClientKeyPath string `json:"key_path"`
CACertPath string `json:"ca_crt_path"`
}
// SetupConfig includes configuration values for initializing
// a keymanager, such as passwords, the wallet, and more.
type SetupConfig struct {
Opts *KeymanagerOpts
MaxMessageSize int
}
// Keymanager implementation using remote signing keys via gRPC.
type Keymanager struct {
opts *KeymanagerOpts
client validatorpb.RemoteSignerClient
orderedPubKeys [][fieldparams.BLSPubkeyLength]byte
accountsChangedFeed *event.Feed
}
// NewKeymanager instantiates a new imported keymanager from configuration options.
func NewKeymanager(_ context.Context, cfg *SetupConfig) (*Keymanager, error) {
// Load the client certificates.
if cfg.Opts.RemoteCertificate == nil {
return nil, errors.New("certificate configuration is missing")
}
var clientCreds credentials.TransportCredentials
if cfg.Opts.RemoteCertificate.RequireTls {
if cfg.Opts.RemoteCertificate.ClientCertPath == "" {
return nil, errors.New("client certificate is required")
}
if cfg.Opts.RemoteCertificate.ClientKeyPath == "" {
return nil, errors.New("client key is required")
}
clientPair, err := tls.LoadX509KeyPair(cfg.Opts.RemoteCertificate.ClientCertPath, cfg.Opts.RemoteCertificate.ClientKeyPath)
if err != nil {
return nil, 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 cfg.Opts.RemoteCertificate.CACertPath != "" {
serverCA, err := os.ReadFile(cfg.Opts.RemoteCertificate.CACertPath)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain server's CA certificate")
}
if !cp.AppendCertsFromPEM(serverCA) {
return nil, errors.Wrap(err, "failed to add server's CA certificate to pool")
}
}
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{clientPair},
RootCAs: cp,
MinVersion: tls.VersionTLS13,
}
clientCreds = credentials.NewTLS(tlsCfg)
}
grpcOpts := []grpc.DialOption{
// Receive large messages without erroring.
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(cfg.MaxMessageSize)),
}
if cfg.Opts.RemoteCertificate.RequireTls {
// Require TLS with client certificate.
grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(clientCreds))
} else {
grpcOpts = append(grpcOpts, grpc.WithInsecure())
}
conn, err := grpc.Dial(cfg.Opts.RemoteAddr, grpcOpts...)
if err != nil {
return nil, errors.New("failed to connect to remote wallet")
}
client := validatorpb.NewRemoteSignerClient(conn)
k := &Keymanager{
opts: cfg.Opts,
client: client,
orderedPubKeys: make([][fieldparams.BLSPubkeyLength]byte, 0),
accountsChangedFeed: new(event.Feed),
}
return k, nil
}
// UnmarshalOptionsFile attempts to JSON unmarshal a keymanager
// options file into a struct.
func UnmarshalOptionsFile(r io.ReadCloser) (*KeymanagerOpts, error) {
enc, err := io.ReadAll(r)
if err != nil {
return nil, errors.Wrap(err, "could not read config")
}
defer func() {
if err := r.Close(); err != nil {
log.Errorf("Could not close keymanager config file: %v", err)
}
}()
opts := &KeymanagerOpts{
RemoteCertificate: &CertificateConfig{RequireTls: true},
}
if err := json.Unmarshal(enc, opts); err != nil {
return nil, errors.Wrap(err, "could not JSON unmarshal")
}
return opts, nil
}
// MarshalOptionsFile for the keymanager.
func MarshalOptionsFile(_ context.Context, cfg *KeymanagerOpts) ([]byte, error) {
return json.MarshalIndent(cfg, "", "\t")
}
// String pretty-print of a remote keymanager options.
func (opts *KeymanagerOpts) String() string {
au := aurora.NewAurora(true)
var b strings.Builder
strAddr := fmt.Sprintf("%s: %s\n", au.BrightMagenta("Remote gRPC address"), opts.RemoteAddr)
if _, err := b.WriteString(strAddr); err != nil {
log.Error(err)
return ""
}
strRequireTls := fmt.Sprintf(
"%s: %t\n", au.BrightMagenta("Require TLS"), opts.RemoteCertificate.RequireTls,
)
if _, err := b.WriteString(strRequireTls); err != nil {
log.Error(err)
return ""
}
strCrt := fmt.Sprintf(
"%s: %s\n", au.BrightMagenta("Client cert path"), opts.RemoteCertificate.ClientCertPath,
)
if _, err := b.WriteString(strCrt); err != nil {
log.Error(err)
return ""
}
strKey := fmt.Sprintf(
"%s: %s\n", au.BrightMagenta("Client key path"), opts.RemoteCertificate.ClientKeyPath,
)
if _, err := b.WriteString(strKey); err != nil {
log.Error(err)
return ""
}
strCa := fmt.Sprintf(
"%s: %s\n", au.BrightMagenta("CA cert path"), opts.RemoteCertificate.CACertPath,
)
if _, err := b.WriteString(strCa); err != nil {
log.Error(err)
return ""
}
return b.String()
}
// KeymanagerOpts for the remote keymanager.
func (km *Keymanager) KeymanagerOpts() *KeymanagerOpts {
return km.opts
}
// ReloadPublicKeys reloads public keys.
func (km *Keymanager) ReloadPublicKeys(ctx context.Context) ([][fieldparams.BLSPubkeyLength]byte, error) {
pubKeys, err := km.FetchValidatingPublicKeys(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not reload public keys")
}
sort.Slice(pubKeys, func(i, j int) bool { return bytes.Compare(pubKeys[i][:], pubKeys[j][:]) == -1 })
if len(km.orderedPubKeys) != len(pubKeys) {
log.Info(keymanager.KeysReloaded)
km.accountsChangedFeed.Send(pubKeys)
} else {
for i := range km.orderedPubKeys {
if !bytes.Equal(km.orderedPubKeys[i][:], pubKeys[i][:]) {
log.Info(keymanager.KeysReloaded)
km.accountsChangedFeed.Send(pubKeys)
break
}
}
}
km.orderedPubKeys = pubKeys
return km.orderedPubKeys, nil
}
// FetchValidatingPublicKeys fetches the list of public keys that should be used to validate with.
func (km *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][fieldparams.BLSPubkeyLength]byte, error) {
resp, err := km.client.ListValidatingPublicKeys(ctx, &empty.Empty{})
if err != nil {
return nil, errors.Wrap(err, "could not list accounts from remote server")
}
pubKeys := make([][fieldparams.BLSPubkeyLength]byte, len(resp.ValidatingPublicKeys))
for i := range resp.ValidatingPublicKeys {
pubKeys[i] = bytesutil.ToBytes48(resp.ValidatingPublicKeys[i])
}
return pubKeys, nil
}
// Sign signs a message for a validator key via a gRPC request.
func (km *Keymanager) Sign(ctx context.Context, req *validatorpb.SignRequest) (bls.Signature, error) {
resp, err := km.client.Sign(ctx, req)
if err != nil {
return nil, err
}
switch resp.Status {
case validatorpb.SignResponse_DENIED:
return nil, ErrSigningDenied
case validatorpb.SignResponse_FAILED:
return nil, ErrSigningFailed
}
return bls.SignatureFromBytes(resp.Signature)
}
// SubscribeAccountChanges creates an event subscription for a channel
// to listen for public key changes at runtime, such as when new validator accounts
// are imported into the keymanager while the validator process is running.
func (km *Keymanager) SubscribeAccountChanges(pubKeysChan chan [][fieldparams.BLSPubkeyLength]byte) event.Subscription {
return km.accountsChangedFeed.Subscribe(pubKeysChan)
}
// ExtractKeystores is not supported for the remote keymanager type.
func (*Keymanager) ExtractKeystores(
ctx context.Context, publicKeys []bls.PublicKey, password string,
) ([]*keymanager.Keystore, error) {
return nil, errors.New("extracting keys not supported for a remote keymanager")
}
// DeleteKeystores is not supported for the remote keymanager type.
func (*Keymanager) DeleteKeystores(context.Context, [][]byte) ([]*ethpbservice.DeletedKeystoreStatus, error) {
return nil, errors.New("Wrong wallet type: web3-signer. Only Imported or Derived wallets can delete accounts")
}
func (km *Keymanager) ListKeymanagerAccounts(ctx context.Context, cfg keymanager.ListKeymanagerAccountConfig) error {
return ListKeymanagerAccountsImpl(ctx, cfg, km, km.KeymanagerOpts())
}
func ListKeymanagerAccountsImpl(ctx context.Context, cfg keymanager.ListKeymanagerAccountConfig, km keymanager.IKeymanager, opts *KeymanagerOpts) error {
au := aurora.NewAurora(true)
fmt.Printf("(keymanager kind) %s\n", au.BrightGreen("remote signer").Bold())
fmt.Printf(
"(configuration file path) %s\n",
au.BrightGreen(filepath.Join(cfg.WalletAccountsDir, cfg.KeymanagerConfigFileName)).Bold(),
)
fmt.Println(" ")
fmt.Printf("%s\n", au.BrightGreen("Configuration options").Bold())
fmt.Println(opts)
validatingPubKeys, err := km.FetchValidatingPublicKeys(ctx)
if err != nil {
return errors.Wrap(err, "could not fetch validating public keys")
}
if len(validatingPubKeys) == 1 {
fmt.Print("Showing 1 validator account\n")
} else if len(validatingPubKeys) == 0 {
fmt.Print("No accounts found\n")
return nil
} else {
fmt.Printf("Showing %d validator accounts\n", len(validatingPubKeys))
}
remote_utils.DisplayRemotePublicKeys(validatingPubKeys)
return nil
}