mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2024-12-25 12:57:18 +00:00
268 lines
8.3 KiB
Go
268 lines
8.3 KiB
Go
|
package keymanager
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"crypto/tls"
|
||
|
"crypto/x509"
|
||
|
"encoding/json"
|
||
|
"io/ioutil"
|
||
|
|
||
|
"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"
|
||
|
)
|
||
|
|
||
|
// 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),
|
||
|
}
|
||
|
|
||
|
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.New("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.SignState_DENIED:
|
||
|
return nil, ErrDenied
|
||
|
case pb.SignState_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,
|
||
|
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.SignState_DENIED:
|
||
|
return nil, ErrDenied
|
||
|
case pb.SignState_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.SignState_DENIED:
|
||
|
return nil, ErrDenied
|
||
|
case pb.SignState_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,
|
||
|
}
|
||
|
accountsResp, err := listerClient.ListAccounts(context.Background(), listAccountsReq)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
accounts := make(map[[48]byte]*accountInfo, len(accountsResp.Accounts))
|
||
|
for _, account := range accountsResp.Accounts {
|
||
|
account := &accountInfo{
|
||
|
Name: account.Name,
|
||
|
PubKey: account.PublicKey,
|
||
|
}
|
||
|
accounts[bytesutil.ToBytes48(account.PubKey)] = account
|
||
|
}
|
||
|
km.accounts = accounts
|
||
|
return nil
|
||
|
}
|