diff --git a/pkg/services/control/ir/server/calls.go b/pkg/services/control/ir/server/calls.go new file mode 100644 index 000000000..11dbe851b --- /dev/null +++ b/pkg/services/control/ir/server/calls.go @@ -0,0 +1,34 @@ +package control + +import ( + "context" + + control "github.com/nspcc-dev/neofs-node/pkg/services/control/ir" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// HealthCheck returns health status of the local IR node. +// +// If request is not signed with a key from white list, permission error returns. +func (s *Server) HealthCheck(_ context.Context, req *control.HealthCheckRequest) (*control.HealthCheckResponse, error) { + // verify request + if err := s.isValidRequest(req); err != nil { + return nil, status.Error(codes.PermissionDenied, err.Error()) + } + + // create and fill response + resp := new(control.HealthCheckResponse) + + body := new(control.HealthCheckResponse_Body) + resp.SetBody(body) + + body.SetHealthStatus(s.prm.healthChecker.HealthStatus()) + + // sign the response + if err := SignMessage(s.prm.key, resp); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return resp, nil +} diff --git a/pkg/services/control/ir/server/deps.go b/pkg/services/control/ir/server/deps.go new file mode 100644 index 000000000..2fdc6751f --- /dev/null +++ b/pkg/services/control/ir/server/deps.go @@ -0,0 +1,13 @@ +package control + +import control "github.com/nspcc-dev/neofs-node/pkg/services/control/ir" + +// HealthChecker is component interface for calculating +// the current health status of a node. +type HealthChecker interface { + // Must calculate and return current health status of the IR application. + // + // If status can not be calculated for any reason, + // control.HealthStatus_HEALTH_STATUS_UNDEFINED should be returned. + HealthStatus() control.HealthStatus +} diff --git a/pkg/services/control/ir/server/opts.go b/pkg/services/control/ir/server/opts.go new file mode 100644 index 000000000..40bdccb67 --- /dev/null +++ b/pkg/services/control/ir/server/opts.go @@ -0,0 +1,20 @@ +package control + +// Option specifies Server's optional parameter. +type Option func(*options) + +type options struct { + allowedKeys [][]byte +} + +func defaultOptions() *options { + return new(options) +} + +// WithAllowedKeys returns option to add public keys +// to white list of the Control service. +func WithAllowedKeys(keys [][]byte) Option { + return func(o *options) { + o.allowedKeys = append(o.allowedKeys, keys...) + } +} diff --git a/pkg/services/control/ir/server/prm.go b/pkg/services/control/ir/server/prm.go new file mode 100644 index 000000000..a73a00c76 --- /dev/null +++ b/pkg/services/control/ir/server/prm.go @@ -0,0 +1,24 @@ +package control + +import ( + "crypto/ecdsa" +) + +// Prm groups required parameters of +// Server's constructor. +type Prm struct { + key *ecdsa.PrivateKey + + healthChecker HealthChecker +} + +// SetPrivateKey sets private key to sign responses. +func (x *Prm) SetPrivateKey(key *ecdsa.PrivateKey) { + x.key = key +} + +// SetHealthChecker sets HealthChecker to calculate +// health status. +func (x *Prm) SetHealthChecker(hc HealthChecker) { + x.healthChecker = hc +} diff --git a/pkg/services/control/ir/server/server.go b/pkg/services/control/ir/server/server.go new file mode 100644 index 000000000..b22982b5b --- /dev/null +++ b/pkg/services/control/ir/server/server.go @@ -0,0 +1,55 @@ +package control + +import ( + "fmt" + + crypto "github.com/nspcc-dev/neofs-crypto" +) + +// Server is an entity that serves +// Control service on IR node. +// +// To gain access to the service, any request must be +// signed with a key from the white list. +type Server struct { + prm Prm + + allowedKeys [][]byte +} + +func panicOnPrmValue(n string, v interface{}) { + const invalidPrmValFmt = "invalid %s parameter (%T): %v" + panic(fmt.Sprintf(invalidPrmValFmt, n, v, v)) +} + +// New creates a new instance of the Server. +// +// Panics if: +// - parameterized private key is nil; +// - parameterized HealthChecker is nil. +// +// Forms white list from all keys specified via +// WithAllowedKeys option and a public key of +// the parameterized private key. +func New(prm Prm, opts ...Option) *Server { + // verify required parameters + switch { + case prm.key == nil: + panicOnPrmValue("key", prm.key) + case prm.healthChecker == nil: + panicOnPrmValue("health checker", prm.healthChecker) + } + + // compute optional parameters + o := defaultOptions() + + for _, opt := range opts { + opt(o) + } + + return &Server{ + prm: prm, + + allowedKeys: append(o.allowedKeys, crypto.MarshalPublicKey(&prm.key.PublicKey)), + } +} diff --git a/pkg/services/control/ir/server/sign.go b/pkg/services/control/ir/server/sign.go new file mode 100644 index 000000000..4b94b7eed --- /dev/null +++ b/pkg/services/control/ir/server/sign.go @@ -0,0 +1,54 @@ +package control + +import ( + "bytes" + "crypto/ecdsa" + "errors" + + "github.com/nspcc-dev/neofs-api-go/util/signature" + control "github.com/nspcc-dev/neofs-node/pkg/services/control/ir" +) + +// SignedMessage is an interface of Control service message. +type SignedMessage interface { + signature.DataSource + GetSignature() *control.Signature + SetSignature(*control.Signature) +} + +var errDisallowedKey = errors.New("key is not in the allowed list") + +func (s *Server) isValidRequest(req SignedMessage) error { + var ( + sign = req.GetSignature() + key = sign.GetKey() + allowed = false + ) + + // check if key is allowed + for i := range s.allowedKeys { + if allowed = bytes.Equal(s.allowedKeys[i], key); allowed { + break + } + } + + if !allowed { + return errDisallowedKey + } + + // verify signature + return signature.VerifyDataWithSource(req, func() ([]byte, []byte) { + return key, sign.GetSign() + }) +} + +// SignMessage signs Control service message with private key. +func SignMessage(key *ecdsa.PrivateKey, msg SignedMessage) error { + return signature.SignDataWithHandler(key, msg, func(key []byte, sig []byte) { + s := new(control.Signature) + s.SetKey(key) + s.SetSign(sig) + + msg.SetSignature(s) + }) +}