diff --git a/.changelog/2686.feature.md b/.changelog/2686.feature.md new file mode 100644 index 00000000000..3debaa44d05 --- /dev/null +++ b/.changelog/2686.feature.md @@ -0,0 +1,5 @@ +go/common/crypto/signature/signers/remote: Add experimental remote signer + +This commit adds an experimental remote signer, reference remote signer +implementation, and allows the node to be ran with a non-file based signer +backend. diff --git a/go/.gitignore b/go/.gitignore index 44537c03bd1..6a8fc2df072 100644 --- a/go/.gitignore +++ b/go/.gitignore @@ -5,4 +5,5 @@ oasis-node/oasis-node oasis-node/integrationrunner/integrationrunner.test oasis-test-runner/oasis-test-runner oasis-net-runner/oasis-net-runner +oasis-remote-signer/oasis-remote-signer storage/mkvs/urkel/interop/urkel-test-helpers diff --git a/go/Makefile b/go/Makefile index 41314203e0d..17e4c5b7a91 100644 --- a/go/Makefile +++ b/go/Makefile @@ -10,7 +10,7 @@ all: build # Build. # List of Go binaries to build. -go-binaries := oasis-node oasis-test-runner oasis-net-runner extra/stats +go-binaries := oasis-node oasis-test-runner oasis-net-runner oasis-remote-signer extra/stats # List of test helpers to build. test-helpers := urkel # List of test vectors to generate. diff --git a/go/common/crypto/signature/signer.go b/go/common/crypto/signature/signer.go index 03968180a66..1f2019819b1 100644 --- a/go/common/crypto/signature/signer.go +++ b/go/common/crypto/signature/signer.go @@ -36,10 +36,19 @@ var ( errUnregisteredContext = errors.New("signature: unregistered context") errNoChainContext = errors.New("signature: chain domain separation context not set") - registeredContexts sync.Map + registeredContexts sync.Map + allowUnregisteredContexts bool chainContextLock sync.RWMutex chainContext Context + + // SignerRoles is the list of all supported signer roles. + SignerRoles = []SignerRole{ + SignerEntity, + SignerNode, + SignerP2P, + SignerConsensus, + } ) type contextOptions struct { @@ -111,6 +120,13 @@ func UnsafeResetChainContext() { chainContext = Context("") } +// UnsafeAllowUnregisteredContexts bypasses the context registration check. +// +// This function is only for the benefit of implementing a remote signer. +func UnsafeAllowUnregisteredContexts() { + allowUnregisteredContexts = true +} + // SetChainContext configures the chain domain separation context that is // used with any contexts constructed using the WithChainSeparation option. func SetChainContext(rawContext string) { @@ -137,10 +153,12 @@ const ( SignerNode SignerP2P SignerConsensus + + // If you add to this, also add the new roles to SignerRoles. ) // SignerFactoryCtor is an SignerFactory constructor. -type SignerFactoryCtor func(interface{}, ...SignerRole) SignerFactory +type SignerFactoryCtor func(interface{}, ...SignerRole) (SignerFactory, error) // SignerFactory is the opaque factory interface for Signers. type SignerFactory interface { @@ -187,6 +205,17 @@ type UnsafeSigner interface { // PrepareSignerContext prepares a context for use during signing by a Signer. func PrepareSignerContext(context Context) ([]byte, error) { + // The remote signer implementation uses the raw context, and + // registration is dealt with client side. Just check that the + // length is sensible, even though the client should be sending + // something sane. + if allowUnregisteredContexts { + if cLen := len(context); cLen == 0 || cLen > ed25519.ContextMaxSize { + return nil, errMalformedContext + } + return []byte(context), nil + } + // Ensure that the context is registered for use. rawOpts, isRegistered := registeredContexts.Load(context) if !isRegistered { diff --git a/go/common/crypto/signature/signers/file/file_signer.go b/go/common/crypto/signature/signers/file/file_signer.go index 872808d1743..48610371b1b 100644 --- a/go/common/crypto/signature/signers/file/file_signer.go +++ b/go/common/crypto/signature/signers/file/file_signer.go @@ -47,16 +47,16 @@ var ( // NewFactory creates a new factory with the specified roles, with the // specified dataDir. -func NewFactory(config interface{}, roles ...signature.SignerRole) signature.SignerFactory { +func NewFactory(config interface{}, roles ...signature.SignerRole) (signature.SignerFactory, error) { dataDir, ok := config.(string) if !ok { - panic("invalid file signer configuration provided") + return nil, errors.New("signature/signer/file: invalid file signer configuration provided") } return &Factory{ roles: append([]signature.SignerRole{}, roles...), dataDir: dataDir, - } + }, nil } // Factory is a PEM file backed SignerFactory. diff --git a/go/common/crypto/signature/signers/file/file_signer_test.go b/go/common/crypto/signature/signers/file/file_signer_test.go index 21e60b1b63a..6fc610b785e 100644 --- a/go/common/crypto/signature/signers/file/file_signer_test.go +++ b/go/common/crypto/signature/signers/file/file_signer_test.go @@ -22,7 +22,8 @@ func TestFileSigner(t *testing.T) { defer os.RemoveAll(tmpDir) rolePEMFiles[signature.SignerUnknown] = "unit_test.pem" - factory := NewFactory(tmpDir, signature.SignerUnknown) + factory, err := NewFactory(tmpDir, signature.SignerUnknown) + require.NoError(err, "NewFactory()") // Missing, no generate. _, err = factory.Load(signature.SignerUnknown) diff --git a/go/common/crypto/signature/signers/ledger/ledger_signer.go b/go/common/crypto/signature/signers/ledger/ledger_signer.go index dd68b315521..77efcab43d8 100644 --- a/go/common/crypto/signature/signers/ledger/ledger_signer.go +++ b/go/common/crypto/signature/signers/ledger/ledger_signer.go @@ -49,16 +49,16 @@ type FactoryConfig struct { } // NewFactory creates a new factory with the specified roles. -func NewFactory(config interface{}, roles ...signature.SignerRole) signature.SignerFactory { +func NewFactory(config interface{}, roles ...signature.SignerRole) (signature.SignerFactory, error) { ledgerConfig, ok := config.(*FactoryConfig) if !ok { - panic("invalid Ledger signer configuration provided") + return nil, fmt.Errorf("invalid Ledger signer configuration provided") } return &Factory{ roles: append([]signature.SignerRole{}, roles...), address: ledgerConfig.Address, index: ledgerConfig.Index, - } + }, nil } // EnsureRole ensures that the SignatureFactory is configured for the given diff --git a/go/common/crypto/signature/signers/remote/grpc.go b/go/common/crypto/signature/signers/remote/grpc.go new file mode 100644 index 00000000000..7f2ff0f73ee --- /dev/null +++ b/go/common/crypto/signature/signers/remote/grpc.go @@ -0,0 +1,277 @@ +// Package remote provides a gRPC backed signer (both client and server). +package remote + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + "github.com/oasislabs/oasis-core/go/common/crypto/signature" + cmnGrpc "github.com/oasislabs/oasis-core/go/common/grpc" +) + +// SignerName is the name used to identify the remote signer. +const SignerName = "remote" + +var ( + serviceName = cmnGrpc.NewServiceName("RemoteSigner") + + methodPublicKeys = serviceName.NewMethod("PublicKeys", nil) + methodSign = serviceName.NewMethod("Sign", SignRequest{}) + + serviceDesc = grpc.ServiceDesc{ + ServiceName: string(serviceName), + HandlerType: (*wrapper)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: methodPublicKeys.ShortName(), + Handler: handlerPublicKeys, + }, + { + MethodName: methodSign.ShortName(), + Handler: handlerSign, + }, + }, + } +) + +// PublicKey is a public key supported by the remote signer. +type PublicKey struct { + Role signature.SignerRole `json:"role"` + PublicKey signature.PublicKey `json:"public_key"` +} + +// SignRequest is a signature request. +type SignRequest struct { + Role signature.SignerRole `json:"role"` + Context string `json:"context"` + Message []byte `json:"message"` +} + +type wrapper struct { + signers map[signature.SignerRole]signature.Signer +} + +func (w *wrapper) publicKeys(ctx context.Context) ([]PublicKey, error) { + var resp []PublicKey + for _, v := range signature.SignerRoles { // Return in consistent order. + if signer := w.signers[v]; signer != nil { + resp = append(resp, PublicKey{ + Role: v, + PublicKey: signer.Public(), + }) + } + } + return resp, nil +} + +func (w *wrapper) sign(ctx context.Context, req *SignRequest) ([]byte, error) { + signer, ok := w.signers[req.Role] + if !ok { + return nil, signature.ErrNotExist + } + return signer.ContextSign(signature.Context(req.Context), req.Message) +} + +func handlerPublicKeys( // nolint: golint + srv interface{}, + ctx context.Context, + dec func(interface{}) error, + interceptor grpc.UnaryServerInterceptor, +) (interface{}, error) { + if interceptor == nil { + return srv.(*wrapper).publicKeys(ctx) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: methodPublicKeys.FullName(), + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(*wrapper).publicKeys(ctx) + } + return interceptor(ctx, nil, info, handler) +} + +func handlerSign( // nolint: golint + srv interface{}, + ctx context.Context, + dec func(interface{}) error, + interceptor grpc.UnaryServerInterceptor, +) (interface{}, error) { + var req SignRequest + if err := dec(&req); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(*wrapper).sign(ctx, &req) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: methodSign.FullName(), + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(*wrapper).sign(ctx, req.(*SignRequest)) + } + return interceptor(ctx, &req, info, handler) +} + +// RegisterService registers a new remote signer backend service with the given +// gRPC server. +// +// WARNING: NEVER call this from the actual node. +func RegisterService(server *grpc.Server, signerFactory signature.SignerFactory) { + // Not sure if this is the best place to do this. + signature.UnsafeAllowUnregisteredContexts() + + // Load all signers, ignoring errors. + w := &wrapper{ + signers: make(map[signature.SignerRole]signature.Signer), + } + for _, v := range signature.SignerRoles { + signer, err := signerFactory.Load(v) + if err == nil { + w.signers[v] = signer + } + } + server.RegisterService(&serviceDesc, w) +} + +type remoteFactory struct { + conn *grpc.ClientConn + reqCtx context.Context + + signers map[signature.SignerRole]*remoteSigner +} + +func (rf *remoteFactory) EnsureRole(role signature.SignerRole) error { + if rf.signers[role] != nil { + return signature.ErrNotExist + } + return nil +} + +func (rf *remoteFactory) Generate(role signature.SignerRole, rng io.Reader) (signature.Signer, error) { + return nil, fmt.Errorf("signature/signer/remote: key re-generation prohibited") +} + +func (rf *remoteFactory) Load(role signature.SignerRole) (signature.Signer, error) { + signer := rf.signers[role] + if signer == nil { + return nil, signature.ErrNotExist + } + return signer, nil +} + +type remoteSigner struct { + factory *remoteFactory + + publicKey signature.PublicKey + role signature.SignerRole +} + +func (rs *remoteSigner) Public() signature.PublicKey { + return rs.publicKey +} + +func (rs *remoteSigner) ContextSign(context signature.Context, message []byte) ([]byte, error) { + // Prepare the context (chain separation is done client side). + rawCtx, err := signature.PrepareSignerContext(context) + if err != nil { + return nil, err + } + + req := &SignRequest{ + Role: rs.role, + Context: string(rawCtx), + Message: message, + } + + var rsp []byte + if err := rs.factory.conn.Invoke(rs.factory.reqCtx, methodSign.FullName(), req, &rsp); err != nil { + return nil, err + } + + return rsp, nil +} + +func (rs *remoteSigner) String() string { + return "[redacted remote private key]" +} + +func (rs *remoteSigner) Reset() { + // Nothing to do. +} + +// FactoryConfig is the remote factory configuration. +type FactoryConfig struct { + // Address is the remote factory gRPC address. + Address string + // ServerCertificate is the server certificate. + ServerCertificate *tls.Certificate + // ClientCertificate is the client certificate. + ClientCertificate *tls.Certificate +} + +// NewFactory creates a new factory with the specified roles. +func NewFactory(config interface{}, roles ...signature.SignerRole) (signature.SignerFactory, error) { + cfg, ok := config.(*FactoryConfig) + if !ok { + return nil, fmt.Errorf("signature/signer/remote: invalid remote signer configuration provided") + } + + if cfg.ServerCertificate == nil { + return nil, fmt.Errorf("signature/signer/remote: server certificate is required") + } + + certPool := x509.NewCertPool() + serverCert, err := x509.ParseCertificate(cfg.ServerCertificate.Certificate[0]) + if err != nil { + return nil, fmt.Errorf("signature/signer/remote: failed to parse server certificate: %w", err) + } + certPool.AddCert(serverCert) + + creds := credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{ + *cfg.ClientCertificate, + }, + RootCAs: certPool, + ServerName: "remote-signer-client", + }) + + conn, err := cmnGrpc.Dial(cfg.Address, grpc.WithTransportCredentials(creds)) + if err != nil { + return nil, fmt.Errorf("signature/signer/remote: failed to dial server: %w", err) + } + + return NewRemoteFactory(context.Background(), conn) +} + +// NewRemoteFactory creates a new gRPC remote signer client service given an +// existing grpc connection. +func NewRemoteFactory(ctx context.Context, conn *grpc.ClientConn) (signature.SignerFactory, error) { + // Enumerate the keys available, and cache them. + var rsp []PublicKey + if err := conn.Invoke(ctx, methodPublicKeys.FullName(), nil, &rsp); err != nil { + return nil, err + } + + rf := &remoteFactory{ + conn: conn, + reqCtx: ctx, + signers: make(map[signature.SignerRole]*remoteSigner), + } + for _, v := range rsp { + rf.signers[v.Role] = &remoteSigner{ + factory: rf, + publicKey: v.PublicKey, + role: v.Role, + } + } + + return rf, nil +} diff --git a/go/common/grpc/auth/auth_tls.go b/go/common/grpc/auth/auth_tls.go new file mode 100644 index 00000000000..aae43c08d25 --- /dev/null +++ b/go/common/grpc/auth/auth_tls.go @@ -0,0 +1,65 @@ +package auth + +import ( + "context" + "crypto/x509" + "fmt" + "sync" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" + + "github.com/oasislabs/oasis-core/go/common/accessctl" +) + +// PeerCertAuthenticator is a server side gRPC authentication function +// that restricts access to all methods based on the hash of the DER +// representation of the client certificate presented in the TLS handshake. +type PeerCertAuthenticator struct { + sync.RWMutex + + whitelist map[accessctl.Subject]bool +} + +// AuthFunc is an AuthenticationFunction backed by the PeerCertAuthenticator. +func (auth *PeerCertAuthenticator) AuthFunc(ctx context.Context, fullMethodName string, req interface{}) error { + peer, ok := peer.FromContext(ctx) + if !ok { + return status.Errorf(codes.PermissionDenied, "grpc: failed to obtain connection peer from context") + } + tlsAuth, ok := peer.AuthInfo.(credentials.TLSInfo) + if !ok { + return status.Errorf(codes.PermissionDenied, "grpc: unexpected peer authentication credentials") + } + if nPeerCerts := len(tlsAuth.State.PeerCertificates); nPeerCerts != 1 { + return status.Errorf(codes.PermissionDenied, fmt.Sprintf("grpc: unexpected number of peer certificates: %d", nPeerCerts)) + } + peerCert := tlsAuth.State.PeerCertificates[0] + subject := accessctl.SubjectFromX509Certificate(peerCert) + + auth.RLock() + defer auth.RUnlock() + if !auth.whitelist[subject] { + return status.Errorf(codes.PermissionDenied, "grpc: unknown peer certificate") + } + + return nil +} + +// AllowPeerCertificate allows a peer certificate access. +func (auth *PeerCertAuthenticator) AllowPeerCertificate(cert *x509.Certificate) { + subject := accessctl.SubjectFromX509Certificate(cert) + + auth.Lock() + defer auth.Unlock() + auth.whitelist[subject] = true +} + +// NewPeerCertAuthenticator creates a new (empty) PeerCertAuthenticator. +func NewPeerCertAuthenticator() *PeerCertAuthenticator { + return &PeerCertAuthenticator{ + whitelist: make(map[accessctl.Subject]bool), + } +} diff --git a/go/common/grpc/grpc.go b/go/common/grpc/grpc.go index f2c49047dd0..df7061fad86 100644 --- a/go/common/grpc/grpc.go +++ b/go/common/grpc/grpc.go @@ -303,7 +303,8 @@ type ServerConfig struct { // nolint: maligned // InstallWrapper specifies whether intercepting facilities should be enabled on this server, // to enable intercepting RPC calls with a wrapper. InstallWrapper bool - AuthFunc auth.AuthenticationFunction + // AuthFunc is the authentication function for access control. + AuthFunc auth.AuthenticationFunction // CustomOptions is an array of extra options for the grpc server. CustomOptions []grpc.ServerOption } @@ -446,7 +447,6 @@ func NewServer(config *ServerConfig) (*Server, error) { // Default to NoAuth. config.AuthFunc = auth.NoAuth } - var sOpts []grpc.ServerOption var wrapper *grpcWrapper unaryInterceptors := []grpc.UnaryServerInterceptor{ logAdapter.unaryLogger, @@ -465,14 +465,14 @@ func NewServer(config *ServerConfig) (*Server, error) { unaryInterceptors = append(unaryInterceptors, wrapper.unaryInterceptor) streamInterceptors = append(streamInterceptors, wrapper.streamInterceptor) } - sOpts = append(sOpts, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unaryInterceptors...))) - sOpts = append(sOpts, grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(streamInterceptors...))) - sOpts = append(sOpts, grpc.MaxRecvMsgSize(maxRecvMsgSize)) - sOpts = append(sOpts, grpc.MaxSendMsgSize(maxSendMsgSize)) - sOpts = append(sOpts, grpc.KeepaliveParams(serverKeepAliveParams)) - sOpts = append(sOpts, grpc.CustomCodec(&CBORCodec{})) - sOpts = append(sOpts, config.CustomOptions...) - + sOpts := []grpc.ServerOption{ + grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unaryInterceptors...)), + grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(streamInterceptors...)), + grpc.MaxRecvMsgSize(maxRecvMsgSize), + grpc.MaxSendMsgSize(maxSendMsgSize), + grpc.KeepaliveParams(serverKeepAliveParams), + grpc.CustomCodec(&CBORCodec{}), + } if config.Certificate != nil { tlsConfig := &tls.Config{ Certificates: []tls.Certificate{*config.Certificate}, @@ -480,6 +480,7 @@ func NewServer(config *ServerConfig) (*Server, error) { } sOpts = append(sOpts, grpc.Creds(credentials.NewTLS(tlsConfig))) } + sOpts = append(sOpts, config.CustomOptions...) return &Server{ BaseBackgroundService: svc, @@ -494,12 +495,13 @@ func NewServer(config *ServerConfig) (*Server, error) { // Dial creates a client connection to the given target. func Dial(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { - opts = append(opts, grpc.WithDefaultCallOptions( - grpc.ForceCodec(&CBORCodec{}), - )) - opts = append(opts, grpc.WithChainUnaryInterceptor(clientUnaryErrorMapper)) - opts = append(opts, grpc.WithChainStreamInterceptor(clientStreamErrorMapper)) - return grpc.Dial(target, opts...) + dialOpts := []grpc.DialOption{ + grpc.WithDefaultCallOptions(grpc.ForceCodec(&CBORCodec{})), + grpc.WithChainUnaryInterceptor(clientUnaryErrorMapper), + grpc.WithChainStreamInterceptor(clientStreamErrorMapper), + } + dialOpts = append(dialOpts, opts...) + return grpc.Dial(target, dialOpts...) } func init() { diff --git a/go/common/identity/identity_test.go b/go/common/identity/identity_test.go index 796f0bfbdb9..6109f654117 100644 --- a/go/common/identity/identity_test.go +++ b/go/common/identity/identity_test.go @@ -16,7 +16,8 @@ func TestLoadOrGenerate(t *testing.T) { require.NoError(t, err, "create data dir") defer os.RemoveAll(dataDir) - factory := fileSigner.NewFactory(dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + factory, err := fileSigner.NewFactory(dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + require.NoError(t, err, "NewFactory") // Generate a new identity. identity, err := LoadOrGenerate(dataDir, factory) diff --git a/go/oasis-node/cmd/common/signer/signer.go b/go/oasis-node/cmd/common/signer/signer.go index f2317270389..48f2e3de4be 100644 --- a/go/oasis-node/cmd/common/signer/signer.go +++ b/go/oasis-node/cmd/common/signer/signer.go @@ -11,6 +11,8 @@ import ( "github.com/oasislabs/oasis-core/go/common/crypto/signature" fileSigner "github.com/oasislabs/oasis-core/go/common/crypto/signature/signers/file" ledgerSigner "github.com/oasislabs/oasis-core/go/common/crypto/signature/signers/ledger" + remoteSigner "github.com/oasislabs/oasis-core/go/common/crypto/signature/signers/remote" + "github.com/oasislabs/oasis-core/go/common/crypto/tls" ) const ( @@ -21,6 +23,11 @@ const ( CfgSignerDir = "signer.dir" cfgSignerLedgerAddress = "signer.ledger.address" cfgSignerLedgerIndex = "signer.ledger.index" + + cfgSignerRemoteAddress = "signer.remote.address" + cfgSignerRemoteClientCert = "signer.remote.client.certificate" + cfgSignerRemoteClientKey = "signer.remote.client.key" + cfgSignerRemoteServerCert = "signer.remote.server.certificate" ) // SignerFlags has the signer-related flags. @@ -57,23 +64,47 @@ func LedgerIndex() uint32 { func NewFactory(signerBackend string, signerDir string, roles ...signature.SignerRole) (signature.SignerFactory, error) { switch signerBackend { case ledgerSigner.SignerName: - config := ledgerSigner.FactoryConfig{ + config := &ledgerSigner.FactoryConfig{ Address: LedgerAddress(), Index: LedgerIndex(), } - return ledgerSigner.NewFactory(&config, roles...), nil + return ledgerSigner.NewFactory(config, roles...) case fileSigner.SignerName: - return fileSigner.NewFactory(signerDir, roles...), nil + return fileSigner.NewFactory(signerDir, roles...) + case remoteSigner.SignerName: + config := &remoteSigner.FactoryConfig{ + Address: viper.GetString(cfgSignerRemoteAddress), + } + clientCert, err := tls.Load( + viper.GetString(cfgSignerRemoteClientCert), + viper.GetString(cfgSignerRemoteClientKey), + ) + if err != nil { + return nil, fmt.Errorf("failed to load client certificate: %w", err) + } + config.ClientCertificate = clientCert + + serverCert, err := tls.LoadCertificate(viper.GetString(cfgSignerRemoteServerCert)) + if err != nil { + return nil, fmt.Errorf("failed to load server certificate: %w", err) + } + config.ServerCertificate = serverCert + + return remoteSigner.NewFactory(config, roles...) default: return nil, fmt.Errorf("unsupported signer backend: %s", signerBackend) } } func init() { - SignerFlags.StringP(CfgSigner, "s", "file", "signer backend [file, ledger]") + SignerFlags.StringP(CfgSigner, "s", "file", "signer backend [file, ledger, remote]") SignerFlags.String(CfgSignerDir, "", "path to directory containing the entity files. If file signer backend is being used, the directory must also contain the private key. If blank, defaults to the working directory.") SignerFlags.String(cfgSignerLedgerAddress, "", "Ledger signer: select Ledger device based on this specified address. If blank, any available Ledger device will be connected to.") SignerFlags.Uint32(cfgSignerLedgerIndex, 0, "Ledger signer: address index used to derive address on Ledger device") + SignerFlags.String(cfgSignerRemoteAddress, "", "remote signer server address") + SignerFlags.String(cfgSignerRemoteClientCert, "", "remote signer client certificate path") + SignerFlags.String(cfgSignerRemoteClientKey, "", "remote signer client certificate key path") + SignerFlags.String(cfgSignerRemoteServerCert, "", "remote signer server certificate path") _ = viper.BindPFlags(SignerFlags) } diff --git a/go/oasis-node/cmd/debug/byzantine/steps.go b/go/oasis-node/cmd/debug/byzantine/steps.go index e3edd27cad2..166b1351bab 100644 --- a/go/oasis-node/cmd/debug/byzantine/steps.go +++ b/go/oasis-node/cmd/debug/byzantine/steps.go @@ -38,7 +38,10 @@ var ( ) func initDefaultIdentity(dataDir string) (*identity.Identity, error) { - signerFactory := fileSigner.NewFactory(dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerEntity, signature.SignerConsensus) + signerFactory, err := fileSigner.NewFactory(dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerEntity, signature.SignerConsensus) + if err != nil { + return nil, errors.Wrap(err, "identity NewFactory") + } id, err := identity.LoadOrGenerate(dataDir, signerFactory) if err != nil { return nil, errors.Wrap(err, "identity LoadOrGenerate") diff --git a/go/oasis-node/cmd/identity/identity.go b/go/oasis-node/cmd/identity/identity.go index 1a40484b069..bb1d814b153 100644 --- a/go/oasis-node/cmd/identity/identity.go +++ b/go/oasis-node/cmd/identity/identity.go @@ -43,9 +43,14 @@ func doNodeInit(cmd *cobra.Command, args []string) { } // Provision the node identity. - nodeSignerFactory := fileSigner.NewFactory(dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) - _, err := identity.LoadOrGenerate(dataDir, nodeSignerFactory) + nodeSignerFactory, err := fileSigner.NewFactory(dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) if err != nil { + logger.Error("failed to create identity signer factory", + "err", err, + ) + os.Exit(1) + } + if _, err = identity.LoadOrGenerate(dataDir, nodeSignerFactory); err != nil { logger.Error("failed to load or generate node identity", "err", err, ) diff --git a/go/oasis-node/cmd/keymanager/keymanager.go b/go/oasis-node/cmd/keymanager/keymanager.go index 716b454c051..baf2ac4a186 100644 --- a/go/oasis-node/cmd/keymanager/keymanager.go +++ b/go/oasis-node/cmd/keymanager/keymanager.go @@ -244,7 +244,12 @@ func signPolicyFromFlags() (*signature.Signature, error) { var signer signature.Signer var err error if viper.GetString(cfgPolicyKeyFile) != "" { - signer, err = fileSigner.NewFactory("", signature.SignerUnknown).(*fileSigner.Factory).ForceLoad(viper.GetString(cfgPolicyKeyFile)) + var signerFactory signature.SignerFactory + signerFactory, err = fileSigner.NewFactory("", signature.SignerUnknown) + if err != nil { + return nil, err + } + signer, err = signerFactory.(*fileSigner.Factory).ForceLoad(viper.GetString(cfgPolicyKeyFile)) if err != nil { return nil, err } diff --git a/go/oasis-node/cmd/registry/node/node.go b/go/oasis-node/cmd/registry/node/node.go index 857ddce366f..1c5a2ca57ef 100644 --- a/go/oasis-node/cmd/registry/node/node.go +++ b/go/oasis-node/cmd/registry/node/node.go @@ -155,7 +155,13 @@ func doInit(cmd *cobra.Command, args []string) { } // Provision the node identity. - nodeSignerFactory := fileSigner.NewFactory(dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + nodeSignerFactory, err := fileSigner.NewFactory(dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + if err != nil { + logger.Error("failed to create node identity signer factory", + "err", err, + ) + os.Exit(1) + } nodeIdentity, err := identity.LoadOrGenerate(dataDir, nodeSignerFactory) if err != nil { logger.Error("failed to load or generate node identity", @@ -346,7 +352,13 @@ func doIsRegistered(cmd *cobra.Command, args []string) { } // Load node's identity. - nodeSignerFactory := fileSigner.NewFactory(dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + nodeSignerFactory, err := fileSigner.NewFactory(dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + if err != nil { + logger.Error("failed to create node identity signer factory", + "err", err, + ) + os.Exit(1) + } nodeIdentity, err := identity.Load(dataDir, nodeSignerFactory) if err != nil { logger.Error("failed to load node identity", diff --git a/go/oasis-node/node_test.go b/go/oasis-node/node_test.go index 46ea22e50df..33a32e0e57c 100644 --- a/go/oasis-node/node_test.go +++ b/go/oasis-node/node_test.go @@ -155,7 +155,8 @@ func newTestNode(t *testing.T) *testNode { dataDir, err := ioutil.TempDir("", "oasis-node-test_") require.NoError(err, "create data dir") - signerFactory := fileSigner.NewFactory(dataDir, signature.SignerEntity) + signerFactory, err := fileSigner.NewFactory(dataDir, signature.SignerEntity) + require.NoError(err, "create file signer") entity, entitySigner, err := entity.Generate(dataDir, signerFactory, nil) require.NoError(err, "create test entity") diff --git a/go/oasis-remote-signer/cmd/root.go b/go/oasis-remote-signer/cmd/root.go new file mode 100644 index 00000000000..174aa325b50 --- /dev/null +++ b/go/oasis-remote-signer/cmd/root.go @@ -0,0 +1,211 @@ +// Package cmd implements the commands for the oasis-remote-signer executable. +package cmd + +import ( + goTls "crypto/tls" + "crypto/x509" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/oasislabs/oasis-core/go/common/crypto/signature" + "github.com/oasislabs/oasis-core/go/common/crypto/signature/signers/file" + "github.com/oasislabs/oasis-core/go/common/crypto/signature/signers/remote" + "github.com/oasislabs/oasis-core/go/common/crypto/tls" + "github.com/oasislabs/oasis-core/go/common/grpc" + "github.com/oasislabs/oasis-core/go/common/grpc/auth" + "github.com/oasislabs/oasis-core/go/common/logging" + "github.com/oasislabs/oasis-core/go/common/version" + cmdCommon "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common" + cmdBackground "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common/background" + cmdGrpc "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common/grpc" +) + +const cfgClientCertificate = "client.certificate" + +var ( + rootCmd = &cobra.Command{ + Use: "oasis-remote-signer", + Short: "Oasis Remote Signer", + Version: version.SoftwareVersion, + RunE: runRoot, + } + + initServerCmd = &cobra.Command{ + Use: "init", + Short: "initialize server keys", + Run: doServerInit, + } + + initClientCmd = &cobra.Command{ + Use: "init_client", + Short: "initialize client certificate", + Run: doClientInit, + } + + rootFlags = flag.NewFlagSet("", flag.ContinueOnError) + + logger = logging.GetLogger("remote-signer") +) + +// Execute spawns the main entry point after handling the config file. +func Execute() { + if err := rootCmd.Execute(); err != nil { + cmdCommon.EarlyLogAndExit(err) + } +} + +func ensureDataDir() (string, error) { + dataDir := cmdCommon.DataDir() + if dataDir == "" { + return "", fmt.Errorf("remote-signer: datadir is mandatory") + } + + return dataDir, nil +} + +func doServerInit(cmd *cobra.Command, args []string) { + if _, _, err := serverInit(); err != nil { + logger.Error("failed to initialize server keys", + "err", err, + ) + os.Exit(1) + } +} + +func serverInit() (signature.SignerFactory, *goTls.Certificate, error) { + dataDir, err := ensureDataDir() + if err != nil { + return nil, nil, err + } + + // Initialize the actual signer. + // TODO: Make the backend configurable (ideally plugin based). + sf, err := file.NewFactory(dataDir, signature.SignerRoles...) + if err != nil { + logger.Error("failed to create signer factory", + "err", err, + ) + return nil, nil, fmt.Errorf("remote-signer: failed to create signer: %w", err) + } + + // Load the server certificate, provisioning if required. + cert, err := tls.LoadOrGenerate( + filepath.Join(dataDir, "remote_signer_server_cert.pem"), + filepath.Join(dataDir, "remote_signer_server_key.pem"), + "remote-signer-server", + ) + if err != nil { + logger.Error("failed to load/generate grpc TLS cert", + "err", err, + ) + return nil, nil, fmt.Errorf("remote-signer: failed to load/generate gRPC TLS certificate: %w", err) + } + + return sf, cert, nil +} + +func doClientInit(cmd *cobra.Command, args []string) { + if err := func() error { + dataDir, err := ensureDataDir() + if err != nil { + return err + } + + _, err = tls.LoadOrGenerate( + filepath.Join(dataDir, "remote_signer_client_cert.pem"), + filepath.Join(dataDir, "remote_signer_client_key.pem"), + "remote-signer-client", + ) + return err + }(); err != nil { + logger.Error("failed to initialize client keys", + "err", err, + ) + os.Exit(1) + } +} + +func runRoot(cmd *cobra.Command, args []string) error { + // Initialize all of the server keys. + sf, cert, err := serverInit() + if err != nil { + logger.Error("failed to initialize server keys", + "err", err, + ) + return err + } + + // Load the client certificate to be granted access. + clientCertPath := viper.GetString(cfgClientCertificate) + tlsCert, err := tls.LoadCertificate(clientCertPath) + if err != nil { + logger.Error("failed to load client TLS certificate", + "err", err, + ) + return err + } + clientCert, err := x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + logger.Error("failed to parse client TLS certificate", + "err", err, + ) + } + peerCertAuth := auth.NewPeerCertAuthenticator() + peerCertAuth.AllowPeerCertificate(clientCert) + + // Initialize the gRPC server. + svr, err := grpc.NewServer(&grpc.ServerConfig{ + Name: "remote-signer", + Port: uint16(viper.GetInt(cmdGrpc.CfgServerPort)), + Certificate: cert, + AuthFunc: peerCertAuth.AuthFunc, + }) + if err != nil { + logger.Error("failed to instantiate gRPC server", + "err", err, + ) + return err + } + remote.RegisterService(svr.Server(), sf) + + // Run the gRPC server. + if err = svr.Start(); err != nil { + logger.Error("failed to start gRPC server", + "err", err, + ) + return err + } + + // Wait for graceful termination. + sm := cmdBackground.NewServiceManager(logger) + sm.Register(svr) + defer sm.Cleanup() + sm.Wait() + + return nil +} + +func init() { + _ = viper.BindPFlags(cmdCommon.RootFlags) + + rootFlags.String(cfgClientCertificate, "client_cert.pem", "client TLS certificate (REQUIRED)") + _ = viper.BindPFlags(rootFlags) + + rootCmd.PersistentFlags().AddFlagSet(cmdCommon.RootFlags) + rootCmd.Flags().AddFlagSet(cmdGrpc.ServerTCPFlags) + rootCmd.Flags().AddFlagSet(rootFlags) + + rootCmd.AddCommand(initServerCmd) + rootCmd.AddCommand(initClientCmd) + + cobra.OnInitialize(func() { + if err := cmdCommon.Init(); err != nil { + cmdCommon.EarlyLogAndExit(err) + } + }) +} diff --git a/go/oasis-remote-signer/main.go b/go/oasis-remote-signer/main.go new file mode 100644 index 00000000000..d5edb1df819 --- /dev/null +++ b/go/oasis-remote-signer/main.go @@ -0,0 +1,10 @@ +// Oasis remote signer implementation. +package main + +import ( + "github.com/oasislabs/oasis-core/go/oasis-remote-signer/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/go/oasis-test-runner/oasis/entity.go b/go/oasis-test-runner/oasis/entity.go index d4b2724d84d..ea42363a73c 100644 --- a/go/oasis-test-runner/oasis/entity.go +++ b/go/oasis-test-runner/oasis/entity.go @@ -168,7 +168,14 @@ func (net *Network) NewEntity(cfg *EntityCfg) (*Entity, error) { net: net, dir: entityDir, } - signerFactory := fileSigner.NewFactory(entityDir.String(), signature.SignerEntity) + signerFactory, err := fileSigner.NewFactory(entityDir.String(), signature.SignerEntity) + if err != nil { + net.logger.Error("failed to create entity file signer factory", + "err", err, + "entity_name", entName, + ) + return nil, errors.Wrap(err, "oasis/entity: failed to create entity file signer") + } ent.entity, ent.entitySigner, err = entity.Load(entityDir.String(), signerFactory) if err != nil { net.logger.Error("failed to load newly provisoned entity", diff --git a/go/oasis-test-runner/oasis/oasis.go b/go/oasis-test-runner/oasis/oasis.go index 46e50451b45..c4ecd02a0f0 100644 --- a/go/oasis-test-runner/oasis/oasis.go +++ b/go/oasis-test-runner/oasis/oasis.go @@ -552,7 +552,10 @@ func (net *Network) generateDeterministicNodeIdentity(dir *env.Dir, rawSeed stri return err } - factory := fileSigner.NewFactory(dir.String(), signature.SignerNode) + factory, err := fileSigner.NewFactory(dir.String(), signature.SignerNode) + if err != nil { + return err + } if _, err = factory.Generate(signature.SignerNode, rng); err != nil { return err } @@ -708,7 +711,10 @@ func (net *Network) provisionNodeIdentity(dataDir *env.Dir, seed string) (signat } } - signerFactory := fileSigner.NewFactory(dataDir.String(), signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + signerFactory, err := fileSigner.NewFactory(dataDir.String(), signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + if err != nil { + return signature.PublicKey{}, errors.Wrap(err, "oasis: failed to create node file signer factory") + } nodeIdentity, err := identity.LoadOrGenerate(dataDir.String(), signerFactory) if err != nil { return signature.PublicKey{}, errors.Wrap(err, "oasis: failed to provision node identity") diff --git a/go/oasis-test-runner/oasis/seed.go b/go/oasis-test-runner/oasis/seed.go index b26fed6cb54..002a0f58c82 100644 --- a/go/oasis-test-runner/oasis/seed.go +++ b/go/oasis-test-runner/oasis/seed.go @@ -54,7 +54,10 @@ func (net *Network) newSeedNode() (*seedNode, error) { // Pre-provision the node identity, so that we can figure out what // to pass all the actual nodes in advance, instead of having to // start the node and fork out to `oasis-node debug tendermint show-node-id`. - signerFactory := fileSigner.NewFactory(seedDir.String(), signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + signerFactory, err := fileSigner.NewFactory(seedDir.String(), signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + if err != nil { + return nil, errors.Wrap(err, "oasis/seed: failed to create seed signer factory") + } seedIdentity, err := identity.LoadOrGenerate(seedDir.String(), signerFactory) if err != nil { return nil, errors.Wrap(err, "oasis/seed: failed to provision seed identity") diff --git a/go/oasis-test-runner/oasis/sentry.go b/go/oasis-test-runner/oasis/sentry.go index 315a90af9a9..093da1f91db 100644 --- a/go/oasis-test-runner/oasis/sentry.go +++ b/go/oasis-test-runner/oasis/sentry.go @@ -104,7 +104,14 @@ func (net *Network) NewSentry(cfg *SentryCfg) (*Sentry, error) { // Pre-provision node's identity to pass the sentry node's consensus // address to the validator so it can configure the sentry node's consensus // address as its consensus address. - signerFactory := fileSigner.NewFactory(sentryDir.String(), signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + signerFactory, err := fileSigner.NewFactory(sentryDir.String(), signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + if err != nil { + net.logger.Error("failed to create sentry signer factory", + "err", err, + "sentry_name", sentryName, + ) + return nil, fmt.Errorf("oasis/sentry: failed to create sentry file signer: %w", err) + } sentryIdentity, err := identity.LoadOrGenerate(sentryDir.String(), signerFactory) if err != nil { net.logger.Error("failed to provision sentry identity", diff --git a/go/oasis-test-runner/scenario/e2e/identity_cli.go b/go/oasis-test-runner/scenario/e2e/identity_cli.go index e23abe39820..a74f407b994 100644 --- a/go/oasis-test-runner/scenario/e2e/identity_cli.go +++ b/go/oasis-test-runner/scenario/e2e/identity_cli.go @@ -77,8 +77,11 @@ func (ident *identityCLIImpl) Run(childEnv *env.Env) error { func (ident *identityCLIImpl) loadIdentity() error { ident.logger.Info("loading generated entity") - factory := fileSigner.NewFactory(ident.dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) - if _, err := identity.Load(ident.dataDir, factory); err != nil { + factory, err := fileSigner.NewFactory(ident.dataDir, signature.SignerNode, signature.SignerP2P, signature.SignerConsensus) + if err != nil { + return fmt.Errorf("failed to create identity file signer: %w", err) + } + if _, err = identity.Load(ident.dataDir, factory); err != nil { return fmt.Errorf("failed to load node's identity: %w", err) } return nil diff --git a/go/oasis-test-runner/scenario/e2e/registry_cli.go b/go/oasis-test-runner/scenario/e2e/registry_cli.go index 916192912ed..60e4de8f617 100644 --- a/go/oasis-test-runner/scenario/e2e/registry_cli.go +++ b/go/oasis-test-runner/scenario/e2e/registry_cli.go @@ -243,7 +243,10 @@ func (r *registryCLIImpl) listEntities(childEnv *env.Env) ([]signature.PublicKey // loadEntity loads entity and signer from given directory. func (r *registryCLIImpl) loadEntity(entDir string) (*entity.Entity, error) { - entitySignerFactory := fileSigner.NewFactory(entDir, signature.SignerEntity) + entitySignerFactory, err := fileSigner.NewFactory(entDir, signature.SignerEntity) + if err != nil { + return nil, fmt.Errorf("failed to create entity file signer: %w", err) + } ent, _, err := entity.Load(entDir, entitySignerFactory) if err != nil { return nil, fmt.Errorf("failed to load entity: %w", err) diff --git a/go/worker/registration/worker.go b/go/worker/registration/worker.go index 38b1e7b6c37..fc9c161fbec 100644 --- a/go/worker/registration/worker.go +++ b/go/worker/registration/worker.go @@ -693,7 +693,10 @@ func GetRegistrationSigner(logger *logging.Logger, dataDir string, identity *ide logger.Warn("using the entity signing key for node registration") - factory := fileSigner.NewFactory(dataDir, signature.SignerEntity) + factory, err := fileSigner.NewFactory(dataDir, signature.SignerEntity) + if err != nil { + return defaultPk, nil, errors.Wrap(err, "worker/registration: failed to create entity signer factory") + } fileFactory := factory.(*fileSigner.Factory) entitySigner, err := fileFactory.ForceLoad(f) if err != nil {