frostfs-node/pkg/services/tree/signature.go
Airat Arifullin 6e82661c35 [#1563] tree: Wrap only ChainRouterError erros with ObjectAccessDenied
* Such wrapping helps to differentiate logical check errors and server internal
  errors.

Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2024-12-16 15:16:07 +03:00

196 lines
5 KiB
Go

package tree
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"errors"
"fmt"
core "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
checkercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/common/ape"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/refs"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"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/user"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)
type message interface {
SignedDataSize() int
ReadSignedData([]byte) ([]byte, error)
GetSignature() *Signature
SetSignature(*Signature)
}
var (
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(ctx context.Context, 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)
}
bt, err := parseBearer(rawBearer, cid)
if err != nil {
return fmt.Errorf("access to operation %s is denied: %w", op, err)
}
role, pubKey, err := roleAndPubKeyFromReq(cnr, req, bt)
if err != nil {
return fmt.Errorf("can't get request role: %w", err)
}
if err = s.checkAPE(ctx, bt, cnr, cid, op, role, pubKey); err != nil {
return apeErr(err)
}
return nil
}
func apeErr(err error) error {
var chRouterErr *checkercore.ChainRouterError
if !errors.As(err, &chRouterErr) {
errServerInternal := &apistatus.ServerInternal{}
apistatus.WriteInternalServerErr(errServerInternal, err)
return errServerInternal
}
errAccessDenied := &apistatus.ObjectAccessDenied{}
errAccessDenied.WriteReason(err.Error())
return errAccessDenied
}
// 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) (*bearer.Token, error) {
if len(rawBearer) == 0 {
return nil, nil
}
bt := new(bearer.Token)
if err := bt.Unmarshal(rawBearer); err != nil {
return nil, fmt.Errorf("invalid bearer token: %w", err)
}
if !bt.AssertContainer(cid) {
return nil, errBearerWrongContainer
}
if !bt.VerifySignature() {
return nil, errBearerSignature
}
return bt, nil
}
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
}