diff --git a/WORKSPACE b/WORKSPACE index 465a0fbb7f74..081c7436d171 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1650,3 +1650,11 @@ go_repository( sum = "h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0=", version = "v1.20.0", ) + +go_repository( + name = "com_github_wealdtech_eth2_signer_api", + build_file_proto_mode = "disable_global", + importpath = "github.com/wealdtech/eth2-signer-api", + sum = "h1:fqJYjKwG/FeUAJYYiZblIP6agiz3WWB+Hxpw85Fnr5I=", + version = "v1.0.1", +) diff --git a/beacon-chain/core/helpers/BUILD.bazel b/beacon-chain/core/helpers/BUILD.bazel index 63eec270e742..ec5a2cf0d5a3 100644 --- a/beacon-chain/core/helpers/BUILD.bazel +++ b/beacon-chain/core/helpers/BUILD.bazel @@ -23,6 +23,7 @@ go_library( "//slasher:__subpackages__", "//tools:__subpackages__", "//validator:__subpackages__", + "//endtoend/evaluators:__pkg__", ], deps = [ "//beacon-chain/cache:go_default_library", diff --git a/validator/client/service.go b/validator/client/service.go index a763d6b3b691..2609df7b9cc6 100644 --- a/validator/client/service.go +++ b/validator/client/service.go @@ -10,6 +10,10 @@ import ( grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" "github.com/pkg/errors" ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" + "github.com/prysmaticlabs/go-ssz" + "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/shared/bls" + "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/prysmaticlabs/prysm/validator/db" "github.com/prysmaticlabs/prysm/validator/keymanager" "github.com/sirupsen/logrus" @@ -177,3 +181,20 @@ func (v *ValidatorService) Status() error { } return nil } + +// signObject signs a generic object, with protection if available. +func (v *validator) signObject(pubKey [48]byte, object interface{}, domain []byte) (*bls.Signature, error) { + if protectingKeymanager, supported := v.keyManager.(keymanager.ProtectingKeyManager); supported { + root, err := ssz.HashTreeRoot(object) + if err != nil { + return nil, err + } + return protectingKeymanager.SignGeneric(pubKey, root, bytesutil.ToBytes32(domain)) + } + + root, err := helpers.ComputeSigningRoot(object, domain) + if err != nil { + return nil, err + } + return v.keyManager.Sign(pubKey, root) +} diff --git a/validator/client/validator_aggregate.go b/validator/client/validator_aggregate.go index 286f46cc2724..6a9a0291659f 100644 --- a/validator/client/validator_aggregate.go +++ b/validator/client/validator_aggregate.go @@ -133,11 +133,7 @@ func (v *validator) signSlot(ctx context.Context, pubKey [48]byte, slot uint64) return nil, err } - root, err := helpers.ComputeSigningRoot(slot, domain.SignatureDomain) - if err != nil { - return nil, err - } - sig, err := v.keyManager.Sign(pubKey, root) + sig, err := v.signObject(pubKey, slot, domain.SignatureDomain) if err != nil { return nil, errors.Wrap(err, "Failed to sign slot") } diff --git a/validator/client/validator_attest.go b/validator/client/validator_attest.go index 9041d0ad3658..66281cdb077d 100644 --- a/validator/client/validator_attest.go +++ b/validator/client/validator_attest.go @@ -213,7 +213,7 @@ func (v *validator) signAtt(ctx context.Context, pubKey [48]byte, data *ethpb.At var sig *bls.Signature if protectingKeymanager, supported := v.keyManager.(keymanager.ProtectingKeyManager); supported { - sig, err = protectingKeymanager.SignAttestation(pubKey, domain.SignatureDomain, data) + sig, err = protectingKeymanager.SignAttestation(pubKey, bytesutil.ToBytes32(domain.SignatureDomain), data) } else { sig, err = v.keyManager.Sign(pubKey, root) } diff --git a/validator/client/validator_propose.go b/validator/client/validator_propose.go index 0c25c732ca60..d2e521504272 100644 --- a/validator/client/validator_propose.go +++ b/validator/client/validator_propose.go @@ -3,7 +3,6 @@ package client // Validator client proposer functions. import ( "context" - "encoding/binary" "fmt" "github.com/pkg/errors" @@ -176,17 +175,11 @@ func (v *validator) ProposeExit(ctx context.Context, exit *ethpb.VoluntaryExit) // Sign randao reveal with randao domain and private key. func (v *validator) signRandaoReveal(ctx context.Context, pubKey [48]byte, epoch uint64) ([]byte, error) { domain, err := v.domainData(ctx, epoch, params.BeaconConfig().DomainRandao[:]) - if err != nil { return nil, errors.Wrap(err, "could not get domain data") } - var buf [32]byte - binary.LittleEndian.PutUint64(buf[:], epoch) - sigRoot, err := helpers.ComputeSigningRoot(epoch, domain.SignatureDomain) - if err != nil { - return nil, errors.Wrap(err, "could not compute signing root") - } - randaoReveal, err := v.keyManager.Sign(pubKey, sigRoot) + + randaoReveal, err := v.signObject(pubKey, epoch, domain.SignatureDomain) if err != nil { return nil, errors.Wrap(err, "could not sign reveal") } @@ -211,7 +204,7 @@ func (v *validator) signBlock(ctx context.Context, pubKey [48]byte, epoch uint64 ParentRoot: b.ParentRoot, BodyRoot: bodyRoot[:], } - sig, err = protectingKeymanager.SignProposal(pubKey, domain.SignatureDomain, blockHeader) + sig, err = protectingKeymanager.SignProposal(pubKey, bytesutil.ToBytes32(domain.SignatureDomain), blockHeader) if err != nil { return nil, errors.Wrap(err, "could not sign block proposal") } diff --git a/validator/keymanager/BUILD.bazel b/validator/keymanager/BUILD.bazel index 2206e248e9dd..384ded3119c3 100644 --- a/validator/keymanager/BUILD.bazel +++ b/validator/keymanager/BUILD.bazel @@ -10,6 +10,7 @@ go_library( "keymanager.go", "log.go", "opts.go", + "remote.go", "wallet.go", ], importpath = "github.com/prysmaticlabs/prysm/validator/keymanager", @@ -19,11 +20,15 @@ go_library( "//shared/bytesutil:go_default_library", "//shared/interop:go_default_library", "//validator/accounts:go_default_library", + "@com_github_pkg_errors//:go_default_library", "@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", + "@com_github_wealdtech_eth2_signer_api//pb/v1:go_default_library", "@com_github_wealdtech_go_eth2_wallet//:go_default_library", "@com_github_wealdtech_go_eth2_wallet_store_filesystem//:go_default_library", "@com_github_wealdtech_go_eth2_wallet_types_v2//:go_default_library", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_grpc//credentials:go_default_library", "@org_golang_x_crypto//ssh/terminal:go_default_library", ], ) @@ -34,12 +39,14 @@ go_test( "direct_interop_test.go", "direct_test.go", "opts_test.go", + "remote_test.go", "wallet_test.go", ], embed = [":go_default_library"], deps = [ "//shared/bls:go_default_library", "//shared/bytesutil:go_default_library", + "//shared/testutil:go_default_library", "@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library", "@com_github_wealdtech_go_eth2_wallet_nd_v2//:go_default_library", "@com_github_wealdtech_go_eth2_wallet_store_filesystem//:go_default_library", diff --git a/validator/keymanager/keymanager.go b/validator/keymanager/keymanager.go index febf711cf35e..f803f84386fb 100644 --- a/validator/keymanager/keymanager.go +++ b/validator/keymanager/keymanager.go @@ -13,22 +13,27 @@ var ErrNoSuchKey = errors.New("no such key") // ErrCannotSign is returned whenever a signing attempt fails. var ErrCannotSign = errors.New("cannot sign") -// ErrCouldSlash is returned whenever a signing attempt is refused due to a potential slashing event. -var ErrCouldSlash = errors.New("could result in a slashing event") +// ErrDenied is returned whenever a signing attempt is denied. +var ErrDenied = errors.New("signing attempt denied") // KeyManager controls access to private keys by the validator. type KeyManager interface { // FetchValidatingKeys fetches the list of public keys that should be used to validate with. FetchValidatingKeys() ([][48]byte, error) // Sign signs a message for the validator to broadcast. + // Note that the domain should already be part of the root, but it is passed along for security purposes. Sign(pubKey [48]byte, root [32]byte) (*bls.Signature, error) } // ProtectingKeyManager provides access to a keymanager that protects its clients from slashing events. type ProtectingKeyManager interface { + // SignGeneric signs a generic root. + // Note that the domain should already be part of the root, but it is provided for authorisation purposes. + SignGeneric(pubKey [48]byte, root [32]byte, domain [32]byte) (*bls.Signature, error) + // SignProposal signs a block proposal for the validator to broadcast. - SignProposal(pubKey [48]byte, domain []byte, data *ethpb.BeaconBlockHeader) (*bls.Signature, error) + SignProposal(pubKey [48]byte, domain [32]byte, data *ethpb.BeaconBlockHeader) (*bls.Signature, error) // SignAttestation signs an attestation for the validator to broadcast. - SignAttestation(pubKey [48]byte, domain []byte, data *ethpb.AttestationData) (*bls.Signature, error) + SignAttestation(pubKey [48]byte, domain [32]byte, data *ethpb.AttestationData) (*bls.Signature, error) } diff --git a/validator/keymanager/remote.go b/validator/keymanager/remote.go new file mode 100644 index 000000000000..675a0acbb093 --- /dev/null +++ b/validator/keymanager/remote.go @@ -0,0 +1,267 @@ +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 /[account name], where the account name can be a + regular expression. If the account specifier is just 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 +} diff --git a/validator/keymanager/remote_test.go b/validator/keymanager/remote_test.go new file mode 100644 index 000000000000..9b0e316c462b --- /dev/null +++ b/validator/keymanager/remote_test.go @@ -0,0 +1,205 @@ +package keymanager_test + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/prysmaticlabs/prysm/shared/testutil" + "github.com/prysmaticlabs/prysm/validator/keymanager" +) + +var validClientCert = `-----BEGIN CERTIFICATE----- +MIIEITCCAgmgAwIBAgIQXUJWQZgVO4IX+zlWGI1/mTANBgkqhkiG9w0BAQsFADAU +MRIwEAYDVQQDEwlBdHRlc3RhbnQwHhcNMjAwMzE3MDgwNjU3WhcNMjEwOTE3MDc1 +OTUyWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAsc977g16Tan2j7YuA+zQOlDntb4Bkfs4sDOznOEvnozHwRZOgfcP +jVcA9AS5eZOGIRrsTssptrgVNDPoIHWoKk7LAKyyLM3dGp5PWeyMBoQA5cq+yPAT +4JkJpDnBFfwxXB99osJH0z3jSTRa62CSVvPRBisK4B9AlLQfcleEQlKJugy9tOAj +G7zodwEi+J4AYQHmOiwL38ZsKq9We5y4HMQ0E7de0FoU5QHrtuPNrTuwVwrq825l +cEAAFey6Btngx+sziysPHWHYOq4xOZ1UPBApeaAFLguzusc/4VwM7kzRNr4VOD8a +eC3CtKLhBBVVxHI5ZlaHS+YylNGYD4+FxQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC +A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQDGCE0 +3k4rHzB+Ycf3pt1MzeDPgzAfBgNVHSMEGDAWgBScIYZa4dQBIW/gVwR0ctGCuHhe +9jANBgkqhkiG9w0BAQsFAAOCAgEAHG/EfvqIwbhYfci+zRCYC7aQPuvhivJblBwN +mbXo2qsxvje1hcKm0ptJLOy/cjJzeLJYREhQlXDPRJC/xgELnbXRjgag82r35+pf +wVJwP6Yw53VCM3o0QKsUrKyMm4sAijOBrJyqpB5untAieZsry5Bfj0S4YobbtdJa +VsEioU07fVVczf5lYN0XrLgRnXq3LMkTiZ6drFiqLkwmXQZVxNujmcaFSm7yCALl +EdhYNmaqedS5me5UOGxwPacrsZwWF9dvMsl3OswgTcaGdsUtx2/q+S2vbZUAM/Gw +qaTanDfvVtVTF7KzVN9hiqKe4mO0HHHK2HWJYBLdRJjInOgRW+53hCmUhLxD+Dq+ +31jLKxn/Y4hyH9E+55b1sJHCFpsbEtVD53fojiH2C/uLbhq4Wr1PXgOoxzf2KeSQ +B3ENu8C4b6AlNhqOnz5zeDcx8Ug0vMfVDAwf6RAYMG5b/MoWNKcLNXhk8H1nbVkt +16ppjh6I27JqfNqfP2J/p3BF++ZugZuWfN9DRaJ6UPz+yyF7eW8fyDAQNl7LS0Kh +8PlF5cYvyIIKVHe38Mn8ZAWboKUs0xNv2vhA9V/4Q1ZzAEkXjmbk8H26sjGvJnvg +Lgm/+6LVWR4EnUlU8aEWASEpTWq2lSRF3ZOvNstHnufyiDfcwDcl/IKKQiVQQ3mX +tw8Jf74= +-----END CERTIFICATE-----` +var validClientKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAsc977g16Tan2j7YuA+zQOlDntb4Bkfs4sDOznOEvnozHwRZO +gfcPjVcA9AS5eZOGIRrsTssptrgVNDPoIHWoKk7LAKyyLM3dGp5PWeyMBoQA5cq+ +yPAT4JkJpDnBFfwxXB99osJH0z3jSTRa62CSVvPRBisK4B9AlLQfcleEQlKJugy9 +tOAjG7zodwEi+J4AYQHmOiwL38ZsKq9We5y4HMQ0E7de0FoU5QHrtuPNrTuwVwrq +825lcEAAFey6Btngx+sziysPHWHYOq4xOZ1UPBApeaAFLguzusc/4VwM7kzRNr4V +OD8aeC3CtKLhBBVVxHI5ZlaHS+YylNGYD4+FxQIDAQABAoIBAQCjV2MVcDQmHDhw +FH95A5bVu3TgM8flfs64rwYU25iPIexuqDs+kOMsh/xMLfrkgGz7BGyIhYGwZLK1 +3ekjyHHPS8qYuAyFtCelSEDE7tRDOAhLEFDq7gCUloGQ561EsQP3CMa1OZwZpgSh +PwM2ruRAFIK0E95NvOfqsv0gYN0Svo7hYjNsvW6ok/ZGMyN2ikcRR04wGOFOGjfT +xTmfURc9ejnOjHAOqLTpToPwM1/gWWR2iMQefC4njy4MO2BXqOPUmHxmmR4PYhu2 +8EcKbyRs+/fvL3GgD3VAlOe5vnkfBzssQhHmexgSk5lHZrcSxUGXYGrYKPAeV2mk +5HRBWp0RAoGBAOUn5w+NCAugcTGP0hfNlyGXsXqUZvnMyFWvUcxgzgPlJyEyDnKn +aIb1DFOF2HckCfLZdrHqqgaF6K3TDvW9BgSKIsvISpo1S95ZPD6DKUo6YQ10CQRW +q/ZZVbxtFksVgFRGYpCVmPNULmx7CiXDT1b/suwNMAwCZwiNPTSvKQVLAoGBAMaj +zDo1/eepRslqnz5s8hh7dGEjfG/ZJcLgAJAxCyAgnIP4Tls7QkNhCVp9LcN6i1bc +CnT6AIuZRXSJWEdp4k2QnVFUmh9Q5MGgwrKYSY5M/1puTISlF1yQ8J6FX8BlDVmy +4dyaSyC0RIvgBzF9/KBDxxmJcHgGQ0awLeeyl4cvAoGBAN83FS3itLmOmXQrofyp +uNNyDeFXeU9OmL5OPqGUkljc+Favib9JLtp3DIC3WfoD0uUJy0LXULN18QaRFnts +mtYFMIvMGE9KJxL5XWOPI8M4Rp1yL+5X9r3Km2cl45dT5GMzBIPOFOTBVU86MtJC +A6C9Bi5FUk4AcRi1a69MB+stAoGAWNiwoyS9IV38dGCFQ4W1LzAg2MXnhZuJoUVR +2yykfkU33Gs2mOXDeKGxblDpJDLumfYnkzSzA72VbE92NdLtTqYtR1Bg8zraZqTC +EOG+nLBh0o/dF8ND1LpbdXvQXRyVwRYaofI9Qi5/LlUQwplIYmKObiSkMnsSok5w +6d5emi8CgYBjtUihOFaAmgqkTHOn4j4eKS1O7/H8QQSVe5M0bocmAIbgJ4At3GnI +E1JcIY2SZtSwAWs6aQPGE42gwsNCCsQWdJNtViO23JbCwlcPToC4aDfc0JJNaYqp +oVV7C5jmJh9VRd2tXIXIZMMNOfThfNf2qDQuJ1S2t5KugozFiRsHUg== +-----END RSA PRIVATE KEY-----` +var validCACert = `-----BEGIN CERTIFICATE----- +MIIE6DCCAtCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwlBdHRl +c3RhbnQwHhcNMjAwMzE3MDc1OTU4WhcNMjEwOTE3MDc1OTUzWjAUMRIwEAYDVQQD +EwlBdHRlc3RhbnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC85Ecg +rLGpidO9yrpXk2mJmahqou+NY3YmaD/h5c4S8OCJrkvbgUKqM6+pZtPJ3P3Dblba +mBsuDJ2TCFU4CBamuSwuxS15HyI9n5rUHGn7NLXbUVkNQRFsYqT4mwgc0wkwhzIm +ZceinUXlEUUVUTcoWoaZnRR5+bk0Dj0nuF2PCTwdMq2UqAUSE+rz1v2/KezWOTae +XUbDpqQ0b6F2dTjg72qPZJXV2J48qJBAxx42q+Bm8eeCFRPG7cdWn35BUa6Ri+S0 +aNPRpV6HqxYel/vnIbgZQ7ukWYeGCaKmOfaQoBGTmjKJ4jZrfKY8u06bIjMAYx6v +lTFBGKf43Sg8Z353dmAXqahSOjbFYMyTFQWOMy5t7elVOr/ZPXfZFquBd5Kb1s1H +6Ef8cd/TZAl7/9bAq8F7cYg4I9JUyy3kbLjI05qfiQGpd/0+zHFraP4WTMbU4g+k +bdWfkTQ4xAz1KY1trhUK7Ur6Bwf9QzbY6xZfDMftSnFzd8oWlspPO3KA23zQoVHH +18TXcM0efLY/xyEArctco2Rx/SNA3z0nY7tLaV1vB3P28y06XvHoUBu482YVbS4E +IMF48ddWSUfbChNZMPa4h8BSQVyrjvdU9R8LwRcaDAIFGWlqUqTIBJEiNoaFoIHK +Xyz3LZmcyZ7S547DDIWl5TcsJtl84GPWILzVowIDAQABo0UwQzAOBgNVHQ8BAf8E +BAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnCGGWuHUASFv4FcE +dHLRgrh4XvYwDQYJKoZIhvcNAQELBQADggIBADXJjjhLrOrdsQLNQgHGL1cXtvmO +uZGbxZXAUPw/rFoL73gPhPm9+c9XSpQETYxNKJk6m4sZSMNNZ3vlrr9TlaIGev3r +06u0/suIj01YM6NAIlHPpflOONQscpEJDvCJEQg5kw/V5AT0OCqsnNholAyhlsjI +4nTAFmR3LY2oMPxgwZY+PrCskvgcNhtR5zh+WxB17TnnKz7yhphWlJHwLfJro928 +nB4thhwYNt3C5Z7tGiX/rA3MW7Sh/H34xC9ISs8ybkwVj+EKjf20FADzjRDF49++ +hqVDxOJw+W0ahQYHBQ/sTkn9S+Cp6PaAw9+efbPG1YuAkCFOWMDKM/yG5tLIpft+ +zASk+VL2WO9oNiJN0rtVwNU//TtENYLS+9p4XTpwEuEIH4ZZApVlEXf0GHKF1x+n +MVH80sXFC9siv5xW76FzvxKv/RZ6fKm8T+uizt8U+jwhL1flS4Ahj7zWUV6cwdWH +57O6FnN+VVNYbV4Ze8SzHS09eS1gBtityJVJttUJk70J/LtMPkaun/+VuMvAha7T +0tPn7P3RbGj8QYVUm+c8Z3arWaJ4K20n3v3rSYtLwV1PpI2T8nL8is7P1AUI4da+ +JW5Xg09Yct1izRb64SylduQC9a1bbjoMU0iABaDzCl7AHzK0RlkjALQ4sIt24nKL +Geq0WUbSP2OuDkAf +-----END CERTIFICATE-----` + +func TestNewRemoteWallet(t *testing.T) { + tests := []struct { + name string + opts string + clientCert string + clientKey string + caCert string + err string + }{ + { + name: "Empty", + opts: ``, + err: "unexpected end of JSON input", + }, + { + name: "NoAccounts", + opts: `{}`, + err: "at least one account specifier is required", + }, + { + name: "NoCertificates", + opts: `{"accounts":["foo"]}`, + err: "certificates are required", + }, + { + name: "NoClientCertificate", + opts: `{"accounts":["foo"],"certificates":{}}`, + err: "client certificate is required", + }, + { + name: "NoClientKey", + opts: `{"accounts":["foo"],"certificates":{"client_cert":"foo"}}`, + err: "client key is required", + }, + { + name: "MissingClientKey", + opts: `{"accounts":["foo"],"certificates":{"client_cert":"foo","client_key":"bar"}}`, + err: "failed to obtain client's certificate and/or key: open foo: no such file or directory", + }, + { + name: "BadClientCert", + clientCert: `bad`, + clientKey: validClientKey, + opts: `{"accounts":["foo"],"certificates":{"client_cert":"<>","client_key":"<>"}}`, + err: "failed to obtain client's certificate and/or key: tls: failed to find any PEM data in certificate input", + }, + { + name: "BadClientKey", + clientCert: validClientCert, + clientKey: `bad`, + opts: `{"accounts":["foo"],"certificates":{"client_cert":"<>","client_key":"<>"}}`, + err: "failed to obtain client's certificate and/or key: tls: failed to find any PEM data in key input", + }, + { + name: "MissingCACert", + clientCert: validClientCert, + clientKey: validClientKey, + opts: `{"accounts":["foo"],"certificates":{"client_cert":"<>","client_key":"<>","ca_cert":"bad"}}`, + err: "failed to obtain server's CA certificate: open bad: no such file or directory", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + if test.caCert != "" || test.clientCert != "" || test.clientKey != "" { + dir := fmt.Sprintf("%s/%s", testutil.TempDir(), test.name) + if err := os.MkdirAll(dir, 0777); err != nil { + t.Fatalf(err.Error()) + } + defer os.RemoveAll(dir) + if test.caCert != "" { + caCertPath := fmt.Sprintf("%s/ca.crt", dir) + if err := ioutil.WriteFile(caCertPath, []byte(test.caCert), 0666); err != nil { + t.Fatalf("Failed to write CA certificate: %v", err) + } + test.opts = strings.ReplaceAll(test.opts, "<>", caCertPath) + } + if test.clientCert != "" { + clientCertPath := fmt.Sprintf("%s/client.crt", dir) + if err := ioutil.WriteFile(clientCertPath, []byte(test.clientCert), 0666); err != nil { + t.Fatalf("Failed to write client certificate: %v", err) + } + test.opts = strings.ReplaceAll(test.opts, "<>", clientCertPath) + } + if test.clientKey != "" { + clientKeyPath := fmt.Sprintf("%s/client.key", dir) + if err := ioutil.WriteFile(clientKeyPath, []byte(test.clientKey), 0666); err != nil { + t.Fatalf("Failed to write client key: %v", err) + } + test.opts = strings.ReplaceAll(test.opts, "<>", clientKeyPath) + } + } + + _, _, err := keymanager.NewRemoteWallet(test.opts) + if test.err == "" { + if err != nil { + t.Fatalf("Received unexpected error: %v", err.Error()) + } + } else { + if err == nil { + t.Fatal("Did not received an error") + } + if err.Error() != test.err { + t.Fatalf("Did not received expected error: expected %v, received %v", test.err, err.Error()) + } + } + }) + } +} diff --git a/validator/node/node.go b/validator/node/node.go index da3753f97f48..766744d994f3 100644 --- a/validator/node/node.go +++ b/validator/node/node.go @@ -247,6 +247,8 @@ func selectKeyManager(ctx *cli.Context) (keymanager.KeyManager, error) { km, help, err = keymanager.NewKeystore(opts) case "wallet": km, help, err = keymanager.NewWallet(opts) + case "remote": + km, help, err = keymanager.NewRemoteWallet(opts) default: return nil, fmt.Errorf("unknown keymanager %q", manager) }