package tree import ( "bytes" "crypto/ecdsa" "crypto/elliptic" "errors" "fmt" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" "git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs" core "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" cidSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto" frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "go.uber.org/zap" ) type message interface { SignedDataSize() int ReadSignedData([]byte) ([]byte, error) GetSignature() *Signature SetSignature(*Signature) } func basicACLErr(op acl.Op) error { return fmt.Errorf("access to operation %s is denied by basic ACL check", op) } func eACLErr(op eacl.Operation, err error) error { return fmt.Errorf("access to operation %s is denied by extended ACL check: %w", op, err) } var ( errBearerWrongOwner = errors.New("bearer token must be signed by the container owner") errBearerWrongContainer = errors.New("bearer token is created for another container") errBearerSignature = errors.New("invalid bearer token signature") ) // verifyClient verifies if the request for a client operation // was signed by a key allowed by (e)ACL rules. // Operation must be one of: // - 1. ObjectPut; // - 2. ObjectGet. func (s *Service) verifyClient(req message, cid cidSDK.ID, rawBearer []byte, op acl.Op) error { err := verifyMessage(req) if err != nil { return err } isAuthorized, err := s.isAuthorized(req, op) if isAuthorized || err != nil { return err } cnr, err := s.cnrSource.Get(cid) if err != nil { return fmt.Errorf("can't get container %s: %w", cid, err) } eaclOp := eACLOp(op) bt, err := parseBearer(rawBearer, cid, eaclOp) if err != nil { return err } role, pubKey, err := roleAndPubKeyFromReq(cnr, req, bt) if err != nil { return fmt.Errorf("can't get request role: %w", err) } basicACL := cnr.Value.BasicACL() // Basic ACL mask can be unset, if a container operations are performed // with strict APE checks only. // // FIXME(@aarifullin): tree service temporiraly performs APE checks on // object verbs, because tree verbs have not been introduced yet. if basicACL == 0x0 { return s.checkAPE(cnr, cid, op, role, pubKey) } if !basicACL.IsOpAllowed(op, role) { return basicACLErr(op) } if !basicACL.Extendable() { return nil } var useBearer bool if len(rawBearer) != 0 { if !basicACL.AllowedBearerRules(op) { s.log.Debug(logs.TreeBearerPresentedButNotAllowedByACL, zap.String("cid", cid.EncodeToString()), zap.Stringer("op", op), ) } else { useBearer = true } } var tb eacl.Table signer := req.GetSignature().GetKey() if useBearer && !bt.Impersonate() { if !bearer.ResolveIssuer(*bt).Equals(cnr.Value.Owner()) { return eACLErr(eaclOp, errBearerWrongOwner) } tb = bt.EACLTable() } else { tbCore, err := s.eaclSource.GetEACL(cid) if err != nil { return handleGetEACLError(err) } tb = *tbCore.Value if useBearer && bt.Impersonate() { signer = bt.SigningKeyBytes() } } return checkEACL(tb, signer, eACLRole(role), eaclOp) } // Returns true iff the operation is read-only and request was signed // with one of the authorized keys. func (s *Service) isAuthorized(req message, op acl.Op) (bool, error) { if op != acl.OpObjectGet { return false, nil } sign := req.GetSignature() if sign == nil { return false, errors.New("missing signature") } key := sign.GetKey() for i := range s.authorizedKeys { if bytes.Equal(s.authorizedKeys[i], key) { return true, nil } } return false, nil } func parseBearer(rawBearer []byte, cid cidSDK.ID, eaclOp eacl.Operation) (*bearer.Token, error) { if len(rawBearer) == 0 { return nil, nil } bt := new(bearer.Token) if err := bt.Unmarshal(rawBearer); err != nil { return nil, eACLErr(eaclOp, fmt.Errorf("invalid bearer token: %w", err)) } if !bt.AssertContainer(cid) { return nil, eACLErr(eaclOp, errBearerWrongContainer) } if !bt.VerifySignature() { return nil, eACLErr(eaclOp, errBearerSignature) } return bt, nil } func handleGetEACLError(err error) error { if client.IsErrEACLNotFound(err) { return nil } return fmt.Errorf("get eACL table: %w", err) } func verifyMessage(m message) error { binBody, err := m.ReadSignedData(nil) if err != nil { return fmt.Errorf("marshal request body: %w", err) } sig := m.GetSignature() // TODO(@cthulhu-rider): #468 use Signature message from FrostFS API to avoid conversion var sigV2 refs.Signature sigV2.SetKey(sig.GetKey()) sigV2.SetSign(sig.GetSign()) sigV2.SetScheme(refs.ECDSA_SHA512) var sigSDK frostfscrypto.Signature if err := sigSDK.ReadFromV2(sigV2); err != nil { return fmt.Errorf("can't read signature: %w", err) } if !sigSDK.Verify(binBody) { return errors.New("invalid signature") } return nil } // SignMessage uses the provided key and signs any protobuf // message that was generated for the TreeService by the // protoc-gen-go-frostfs generator. Returns any errors directly. func SignMessage(m message, key *ecdsa.PrivateKey) error { binBody, err := m.ReadSignedData(nil) if err != nil { return err } keySDK := frostfsecdsa.Signer(*key) data, err := keySDK.Sign(binBody) if err != nil { return err } rawPub := make([]byte, keySDK.Public().MaxEncodedSize()) rawPub = rawPub[:keySDK.Public().Encode(rawPub)] m.SetSignature(&Signature{ Key: rawPub, Sign: data, }) return nil } func roleAndPubKeyFromReq(cnr *core.Container, req message, bt *bearer.Token) (acl.Role, *keys.PublicKey, error) { role := acl.RoleOthers owner := cnr.Value.Owner() rawKey := req.GetSignature().GetKey() if bt != nil && bt.Impersonate() { rawKey = bt.SigningKeyBytes() } pub, err := keys.NewPublicKeyFromBytes(rawKey, elliptic.P256()) if err != nil { return role, nil, fmt.Errorf("invalid public key: %w", err) } var reqSigner user.ID user.IDFromKey(&reqSigner, (ecdsa.PublicKey)(*pub)) if reqSigner.Equals(owner) { role = acl.RoleOwner } return role, pub, nil } func eACLOp(op acl.Op) eacl.Operation { switch op { case acl.OpObjectGet: return eacl.OperationGet case acl.OpObjectPut: return eacl.OperationPut default: panic(fmt.Sprintf("unexpected tree service ACL operation: %s", op)) } } func eACLRole(role acl.Role) eacl.Role { switch role { case acl.RoleOwner: return eacl.RoleUser case acl.RoleOthers: return eacl.RoleOthers default: panic(fmt.Sprintf("unexpected tree service ACL role: %s", role)) } } var ( errDENY = errors.New("DENY eACL rule") errNoAllowRules = errors.New("not found allowing rules for the request") ) // checkEACL searches for the eACL rules that could be applied to the request // (a tuple of a signer key, his FrostFS role and a request operation). // It does not filter the request by the filters of the eACL table since tree // requests do not contain any "object" information that could be filtered and, // therefore, filtering leads to unexpected results. // The code was copied with the minor updates from the SDK repo: // https://github.com/nspcc-dev/frostfs-sdk-go/blob/43a57d42dd50dc60465bfd3482f7f12bcfcf3411/eacl/validator.go#L28. func checkEACL(tb eacl.Table, signer []byte, role eacl.Role, op eacl.Operation) error { for _, record := range tb.Records() { // check type of operation if record.Operation() != op { continue } // check target if !targetMatches(record, role, signer) { continue } switch a := record.Action(); a { case eacl.ActionAllow: return nil case eacl.ActionDeny: return eACLErr(op, errDENY) default: return eACLErr(op, fmt.Errorf("unexpected action: %s", a)) } } return eACLErr(op, errNoAllowRules) } func targetMatches(rec eacl.Record, role eacl.Role, signer []byte) bool { for _, target := range rec.Targets() { // check public key match if pubs := target.BinaryKeys(); len(pubs) != 0 { for _, key := range pubs { if bytes.Equal(key, signer) { return true } } continue } // check target group match if role == target.Role() { return true } } return false }