package server import ( "bytes" "context" "crypto/ecdsa" "encoding/hex" "errors" "fmt" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/control" frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto" frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa" engine "git.frostfs.info/TrueCloudLab/policy-engine" "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) type Server struct { *cfg } // Option of the Server's constructor. type Option func(*cfg) type cfg struct { key *ecdsa.PrivateKey log *zap.Logger allowedKeys [][]byte chainStorage engine.CachedChainStorage } func defaultCfg() *cfg { return &cfg{} } // New creates, initializes and returns new Server instance. func New(opts ...Option) *Server { c := defaultCfg() for _, opt := range opts { opt(c) } if c.log == nil { c.log = zap.NewNop() } c.log = c.log.With(zap.String("service", "control API")) if c.chainStorage == nil { c.chainStorage = engine.NewInMemory() } return &Server{ cfg: c, } } // WithKey returns option to set private key // used for signing responses. func WithKey(key *ecdsa.PrivateKey) Option { return func(c *cfg) { c.key = key } } // WithAuthorizedKeys returns option to add list of public // keys that have rights to use Control service. func WithAuthorizedKeys(keys [][]byte) Option { return func(c *cfg) { c.allowedKeys = append(c.allowedKeys, keys...) } } // WithLogger returns option to set logger. func WithLogger(log *zap.Logger) Option { return func(c *cfg) { c.log = log } } // WithChainStorage returns option to set logger. func WithChainStorage(chainStorage engine.CachedChainStorage) Option { return func(c *cfg) { c.chainStorage = chainStorage } } // HealthCheck returns health status of the local node. // // If request is unsigned or signed by disallowed key, permission error returns. func (s *Server) HealthCheck(_ context.Context, req *control.HealthCheckRequest) (*control.HealthCheckResponse, error) { s.log.Info("healthcheck", zap.String("key", hex.EncodeToString(req.Signature.Key))) // verify request if err := s.isValidRequest(req); err != nil { return nil, status.Error(codes.PermissionDenied, err.Error()) } resp := &control.HealthCheckResponse{ Body: &control.HealthCheckResponse_Body{ HealthStatus: control.HealthStatus_READY, }, } return resp, nil } // AddPolicy adds new policy. // // If request is unsigned or signed by disallowed key, permission error returns. func (s *Server) AddPolicy(_ context.Context, req *control.AddPolicyRequest) (*control.AddPolicyResponse, error) { s.log.Info("add policy", zap.String("key", hex.EncodeToString(req.Signature.Key))) // verify request if err := s.isValidRequest(req); err != nil { return nil, status.Error(codes.PermissionDenied, err.Error()) } req.GetBody() var chain engine.Chain if err := chain.DecodeBytes(req.Body); err != nil { return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("failed to parse body: %s", err.Error())) } s.chainStorage.AddNameSpaceChain(engine.Ingress, "root", &chain) return &control.AddPolicyResponse{}, nil } // SignedMessage is an interface of Control service message. type SignedMessage interface { GetSignature() *control.Signature GetBody() []byte } var errDisallowedKey = errors.New("key is not in the allowed list") var errMissingSignature = errors.New("missing signature") var errInvalidSignature = errors.New("invalid signature") func (s *Server) isValidRequest(req SignedMessage) error { sign := req.GetSignature() if sign == nil { return errMissingSignature } var ( 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 // TODO(@cthulhu-rider): #468 use Signature message from FrostFS API to avoid conversion var sigV2 refs.Signature sigV2.SetKey(sign.GetKey()) sigV2.SetSign(sign.GetSign()) sigV2.SetScheme(refs.ECDSA_SHA512) var sig frostfscrypto.Signature if err := sig.ReadFromV2(sigV2); err != nil { return fmt.Errorf("can't read signature: %w", err) } if !sig.Verify(req.GetBody()) { return errInvalidSignature } return nil } // SignMessage signs Control service message with private key. func SignMessage(key *ecdsa.PrivateKey, body []byte) (*control.Signature, error) { var sig frostfscrypto.Signature err := sig.Calculate(frostfsecdsa.Signer(*key), body) if err != nil { return nil, fmt.Errorf("calculate signature: %w", err) } // TODO(@cthulhu-rider): #468 use Signature message from FrostFS API to avoid conversion var sigV2 refs.Signature sig.WriteToV2(&sigV2) var sigControl control.Signature sigControl.Key = sigV2.GetKey() sigControl.Sign = sigV2.GetSign() return &sigControl, nil }