Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Changelog for NeoFS Node
- Unnecessary local shard relocation of EC parts by policer (#4236)
- GC stalling on expired objects protected by LOCKs stored only on other shards (#4237)
- Unchecked known payload length of unfinished objects in PUT (#4233)
- IR validates original container API requests via contract calls (#4245)

### Changed
- SN now requires requests to have API version >= 2.22 (#4212)
Expand All @@ -41,7 +42,7 @@ Changelog for NeoFS Node
- Support for `Created` and `Removed` events of Container contract in SN (#4242)

### Updated
- `github.com/nspcc-dev/neofs-sdk-go` module to `v1.0.0-rc.23.0.20261021092246-abe302e8498b` (#4194, #4219, #4228, #4224, #4222, #4240)
- `github.com/nspcc-dev/neofs-sdk-go` module to `v1.0.0-rc.23.0.20261021092246-e754fb0cb9bf` (#4194, #4219, #4228, #4224, #4222, #4240, #4245)
- `github.com/nspcc-dev/neofs-contract` module to `v0.28.0` (#4239, #4246, #4250)

### Updating from v0.57.0
Expand Down
9 changes: 6 additions & 3 deletions cmd/neofs-node/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,12 +532,13 @@ func (x *containersInChain) List(id user.ID) ([]cid.ID, error) {
return x.cnrLst.List(&id)
}

func (x *containersInChain) Put(ctx context.Context, cnr container.Container, pub, sig []byte, st []byte, eACL *eacl.Table, eaclPub, eaclSig []byte) (cid.ID, error) {
func (x *containersInChain) Put(ctx context.Context, cnr container.Container, pub, sig []byte, st []byte, eACL *eacl.Table, eaclPub, eaclSig []byte, origReq []byte) (cid.ID, error) {
var prm cntClient.PutPrm
prm.SetContainer(cnr)
prm.SetKey(pub)
prm.SetSignature(sig)
prm.SetToken(st)
prm.SetOriginalRequest(origReq)
if eACL != nil {
prm.SetEACLTable(*eACL, eaclPub, eaclSig)
}
Expand All @@ -550,12 +551,13 @@ func (x *containersInChain) Put(ctx context.Context, cnr container.Container, pu
return id, err
}

func (x *containersInChain) Delete(ctx context.Context, id cid.ID, pub, sig []byte, st []byte) error {
func (x *containersInChain) Delete(ctx context.Context, id cid.ID, pub, sig []byte, st []byte, origReq []byte) error {
var prm cntClient.DeletePrm
prm.SetCID(id[:])
prm.SetSignature(sig)
prm.SetKey(pub)
prm.SetToken(st)
prm.SetOriginalRequest(origReq)

err := x.cCli.Delete(ctx, prm)
if errors.Is(err, client.ErrTxAwaitTimeout) {
Expand All @@ -565,12 +567,13 @@ func (x *containersInChain) Delete(ctx context.Context, id cid.ID, pub, sig []by
return err
}

func (x *containersInChain) PutEACL(ctx context.Context, eACL eacl.Table, pub, sig []byte, st []byte) error {
func (x *containersInChain) PutEACL(ctx context.Context, eACL eacl.Table, pub, sig []byte, st []byte, origReq []byte) error {
var prm cntClient.PutEACLPrm
prm.SetTable(eACL.Marshal())
prm.SetKey(pub)
prm.SetSignature(sig)
prm.SetToken(st)
prm.SetOriginalRequest(origReq)

err := x.cCli.PutEACL(ctx, prm)
if errors.Is(err, client.ErrTxAwaitTimeout) {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ require (
github.com/nspcc-dev/neo-go v0.123.0
github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea
github.com/nspcc-dev/neofs-contract v0.28.0
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.23.0.20261021092246-abe302e8498b
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.23.0.20261026124529-e754fb0cb9bf
github.com/nspcc-dev/tzhash v1.8.4
github.com/panjf2000/ants/v2 v2.12.1
github.com/prometheus/client_golang v1.24.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea h1:mK
github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea/go.mod h1:YzhD4EZmC9Z/PNyd7ysC7WXgIgURc9uCG1UWDeV027Y=
github.com/nspcc-dev/neofs-contract v0.28.0 h1:Ug/vxeM26yJdFG0ix8DGne4M1OHFnGSx48fvEQFNMvA=
github.com/nspcc-dev/neofs-contract v0.28.0/go.mod h1:yolNeVnmpZoCJsEMuRlmenpJHlzw1dQqGc3HuVIYNpg=
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.23.0.20261021092246-abe302e8498b h1:PGE+MSQID0gl+mUaiL1chxtd9O7klkXBhgLVGszwjyM=
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.23.0.20261021092246-abe302e8498b/go.mod h1:8Nz0EKfdWZVIyq7bEtus0A0j0SCewKX9IfOAeZW5G+A=
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.23.0.20261026124529-e754fb0cb9bf h1:1WFtroG/7OrBUMaaRZBwo+Q9ouel5/f4ig1wmWoBoMk=
github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.23.0.20261026124529-e754fb0cb9bf/go.mod h1:8Nz0EKfdWZVIyq7bEtus0A0j0SCewKX9IfOAeZW5G+A=
github.com/nspcc-dev/rfc6979 v0.2.4 h1:NBgsdCjhLpEPJZqmC9rciMZDcSY297po2smeaRjw57k=
github.com/nspcc-dev/rfc6979 v0.2.4/go.mod h1:86ylDw6Kss+P6v4QAJqo1Sp3mC0/Zr9G97xSjQ9TuFg=
github.com/nspcc-dev/tzhash v1.8.4 h1:lvuPGWsqEo9dVEvo/kdNLKv/Cy0yxRs9z5hJp8VcBuo=
Expand Down
215 changes: 200 additions & 15 deletions pkg/innerring/processors/container/process_container.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package container

import (
"bytes"
"errors"
"fmt"
"slices"
"strings"
"time"

"github.com/nspcc-dev/neo-go/pkg/core/transaction"
cntClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/container"
containerEvent "github.com/nspcc-dev/neofs-node/pkg/morph/event/container"
"github.com/nspcc-dev/neofs-node/pkg/services/util"
"github.com/nspcc-dev/neofs-sdk-go/client"
"github.com/nspcc-dev/neofs-sdk-go/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
protocontainer "github.com/nspcc-dev/neofs-sdk-go/proto/container"
protoencoding "github.com/nspcc-dev/neofs-sdk-go/proto/encoding"
protosession "github.com/nspcc-dev/neofs-sdk-go/proto/session"
"github.com/nspcc-dev/neofs-sdk-go/session"
sessionv2 "github.com/nspcc-dev/neofs-sdk-go/session/v2"
"github.com/nspcc-dev/neofs-sdk-go/version"
Expand All @@ -36,7 +42,7 @@ func (cp *Processor) processCreateContainerRequest(req containerEvent.CreateCont
cnrBytes := cnr.Marshal()
id := cid.NewFromMarshalledContainer(cnrBytes)

err = cp.checkPutContainer(cnr, cnrBytes, req.SessionToken, req.InvocationScript, req.VerificationScript)
apiReq, err := cp.checkPutContainer(cnr, cnrBytes, req)
if err != nil {
cp.log.Error("container creation request failed check",
zap.Stringer("container", id), zap.Error(err))
Expand All @@ -57,8 +63,27 @@ func (cp *Processor) processCreateContainerRequest(req containerEvent.CreateCont
zap.Stringer("cid", id), zap.Stringer("cidInEACL", table.GetCID()))
return
}
if apiReq != nil {
// validate that the table that passed via contracts is the same that
// was attached to the request originally
apiEACL := apiReq.GetBody().GetEacl()
if apiEACL == nil {
cp.log.Error("container contract put with additional eACL but original request does not have it",
zap.Stringer("cid", id))
return
}

b := make([]byte, apiEACL.MarshaledSize())
apiEACL.MarshalStable(b)

if !bytes.Equal(req.EACLTable.EACL, b) {
cp.log.Error("container contract put have different eACL from contract and API request",
zap.Stringer("cid", id))
return
}
}

err = cp.checkSetEACL(*req.EACLTable, table, id, cnr)
err = cp.checkSetEACL(*req.EACLTable, table, id, cnr, req.OriginalRequest == nil)
Comment thread
roman-khimov marked this conversation as resolved.
if err != nil {
cp.log.Error("additional eACL with container creation check failed",
zap.Stringer("cid", id), zap.Error(err))
Expand All @@ -84,21 +109,21 @@ var allowedSystemAttributes = map[string]struct{}{
sysAttrChainMeta: {},
}

func (cp *Processor) checkPutContainer(cnr container.Container, cnrBytes, sessionToken, invocScript, verifScript []byte) error {
func (cp *Processor) checkPutContainer(cnr container.Container, cnrBytes []byte, req containerEvent.CreateContainerV2Request) (*protocontainer.PutRequest, error) {
if ver := cnr.Version(); ver.Compare(minContainerAPIVersion) < 0 {
return fmt.Errorf("API version is %s while %s is minimum supported", ver, minContainerAPIVersion)
return nil, fmt.Errorf("API version is %s while %s is minimum supported", ver, minContainerAPIVersion)
}

var metaEnabled bool
for k := range cnr.Attributes() {
if strings.HasPrefix(k, sysAttrPrefix) {
if _, ok := allowedSystemAttributes[k]; !ok {
return fmt.Errorf("system attribute %s is not allowed", k)
return nil, fmt.Errorf("system attribute %s is not allowed", k)
}

if k == sysAttrChainMeta {
if !cp.metaEnabled {
return errors.New("chain meta data attribute is not allowed")
return nil, errors.New("chain meta data attribute is not allowed")
}
metaEnabled = true
}
Expand All @@ -107,33 +132,51 @@ func (cp *Processor) checkPutContainer(cnr container.Container, cnrBytes, sessio

ecRules := cnr.PlacementPolicy().ECRules()
if !cp.allowEC && len(ecRules) > 0 {
return errors.New("EC rules are not supported yet")
return nil, errors.New("EC rules are not supported yet")
}
if len(ecRules) > 0 && cnr.PlacementPolicy().NumberOfReplicas() > 0 {
return errors.New("REP+EC rules are not supported yet")
return nil, errors.New("REP+EC rules are not supported yet")
}
if metaEnabled && cnr.PlacementPolicy().Initial() != nil {
return errors.New("initial placement policies with consistent metadata are not supported yet")
return nil, errors.New("initial placement policies with consistent metadata are not supported yet")
}

if req.OriginalRequest != nil {
var apiReq protocontainer.PutRequest
err := protoencoding.UnmarshalMessage(req.OriginalRequest, &apiReq)
if err != nil {
return nil, fmt.Errorf("could not unmarshal original request: %w", err)
}

var (
body = apiReq.GetBody()
mCnr = body.GetContainer()
)
if mCnr == nil || !bytes.Equal(cnrBytes, protoencoding.MarshalMessage(mCnr)) {
return nil, fmt.Errorf("container in the original request does not match the contract one")
}

return &apiReq, cp.checkOriginalRequest(cnr, cid.ID{}, session.VerbContainerPut, sessionv2.VerbContainerPut, body, apiReq.GetMetaHeader(), apiReq.GetVerifyHeader())
}

err := cp.verifySignature(signatureVerificationData{
ownerContainer: cnr.Owner(),
verb: session.VerbContainerPut,
verbV2: sessionv2.VerbContainerPut,
binTokenSession: sessionToken,
verifScript: verifScript,
invocScript: invocScript,
binTokenSession: req.SessionToken,
verifScript: req.VerificationScript,
invocScript: req.InvocationScript,
signedData: cnrBytes,
})
if err != nil {
return fmt.Errorf("auth container creation: %w", err)
return nil, fmt.Errorf("auth container creation: %w", err)
}

if err = cnr.PlacementPolicy().Verify(); err != nil {
return fmt.Errorf("invalid storage policy: %w", err)
return nil, fmt.Errorf("invalid storage policy: %w", err)
}

return nil
return nil, nil
}

func (cp *Processor) approvePutContainer(mainTx transaction.Transaction, cnr container.Container, id cid.ID) {
Expand Down Expand Up @@ -247,6 +290,20 @@ func (cp *Processor) checkDeleteContainer(req containerEvent.RemoveContainerRequ
return fmt.Errorf("could not receive the container: %w", err)
}

if req.OriginalRequest != nil {
var origReq protocontainer.DeleteRequest
err := protoencoding.UnmarshalMessage(req.OriginalRequest, &origReq)
if err != nil {
return fmt.Errorf("could not unmarshal original request: %w", err)
}
var body = origReq.GetBody()
if !bytes.Equal(req.ID, body.GetContainerId().GetValue()) {
return fmt.Errorf("container ID in the original request does not match the contract one")
}

return cp.checkOriginalRequest(cnr, idCnr, session.VerbContainerDelete, sessionv2.VerbContainerDelete, body, origReq.GetMetaHeader(), origReq.GetVerifyHeader())
}

err = cp.verifySignature(signatureVerificationData{
ownerContainer: cnr.Owner(),
verb: session.VerbContainerDelete,
Expand All @@ -265,6 +322,134 @@ func (cp *Processor) checkDeleteContainer(req containerEvent.RemoveContainerRequ
return nil
}

func (cp *Processor) checkOriginalRequest(cnr container.Container, cnrID cid.ID, verbV1 session.ContainerVerb, verbV2 sessionv2.Verb, reqBody protoencoding.Message, reqMetaHeader *protosession.RequestMetaHeader, reqVerifHeader *protosession.RequestVerificationHeader) error {
if err := util.VerifyRequestAPIVersion(reqMetaHeader.GetVersion()); err != nil {
return fmt.Errorf("invalid request API version: %w", err)
}
if err := util.VerifyRequestValidityTime(cp.chainTime.Now(), reqMetaHeader); err != nil {
return fmt.Errorf("invalid request validity time: %w", err)
}

var sTok []byte
switch {
case reqMetaHeader.GetSessionToken() != nil:
t := reqMetaHeader.GetSessionToken()
sTok = make([]byte, t.MarshaledSize())
t.MarshalStable(sTok)
case reqMetaHeader.GetSessionTokenV2() != nil:
t := reqMetaHeader.GetSessionTokenV2()
sTok = make([]byte, t.MarshaledSize())
t.MarshalStable(sTok)
default:
}

var (
signedData []byte
sig interface {
GetKey() []byte
GetSign() []byte
}
verifData = signatureVerificationData{
ownerContainer: cnr.Owner(),
verb: verbV1,
verbV2: verbV2,
idContainerSet: !cnrID.IsZero(),
idContainer: cnrID,
binTokenSession: sTok,
}
)

// API 2.28- special field request check: this is the only thing that
// signed by owner (of with session issued by the owner) in practice
// see https://github.com/nspcc-dev/neofs-api/pull/446,
// https://github.com/nspcc-dev/neofs-sdk-go/pull/898
if specialFieldSigned(reqMetaHeader) {
switch b := reqBody.(type) {
case *protocontainer.PutRequest_Body:
signedData = cnr.Marshal()
sig = b.GetSignature()
case *protocontainer.DeleteRequest_Body:
signedData = b.GetContainerId().GetValue()
sig = b.GetSignature()
case *protocontainer.SetExtendedACLRequest_Body:
t := b.GetEacl()
signedData = make([]byte, t.MarshaledSize())
b.GetEacl().MarshalStable(signedData)

sig = b.GetSignature()
default:
panic(fmt.Sprintf("unexpected body in container service check: %v", b))
}
} else {
signedData, _ = protoencoding.EncodeRequest(nil, reqBody, reqMetaHeader)
sig = reqVerifHeader.GetRequestSignature()
}

verifData.verifScript = sig.GetKey()
verifData.invocScript = sig.GetSign()
verifData.signedData = signedData

err := cp.verifySignature(verifData)
if err != nil {
return fmt.Errorf("auth error: %w", err)
}

if !multipleReqSignatures(reqMetaHeader) {
return nil
Comment thread
roman-khimov marked this conversation as resolved.
}

// API 2.25- requests.

var (
bodyLen = reqBody.MarshaledSize()
mHeaderLen = reqMetaHeader.MarshaledSize()
needBuff = max(bodyLen, mHeaderLen)
)
signedData = slices.Grow(verifData.signedData[:0], needBuff)

// body

signedData = signedData[:bodyLen]
reqBody.MarshalStable(signedData)

sig = reqVerifHeader.GetBodySignature()

verifData.signedData = signedData
verifData.verifScript = sig.GetKey()
verifData.invocScript = sig.GetSign()

err = cp.verifySignature(verifData)
Comment thread
roman-khimov marked this conversation as resolved.
if err != nil {
return fmt.Errorf("body signature auth error: %w", err)
}

// meta header

signedData = signedData[:mHeaderLen]
reqMetaHeader.MarshalStable(signedData)

sig = reqVerifHeader.GetMetaSignature()

verifData.signedData = signedData
verifData.verifScript = sig.GetKey()
verifData.invocScript = sig.GetSign()

err = cp.verifySignature(verifData)
if err != nil {
return fmt.Errorf("meta header auth error: %w", err)
}

return nil
}

func multipleReqSignatures(m *protosession.RequestMetaHeader) bool {
return m == nil || m.Version == nil || m.Version.Major < 2 || (m.Version.Major == 2 && m.Version.Minor < 26)
}

func specialFieldSigned(m *protosession.RequestMetaHeader) bool {
return m == nil || m.Version == nil || m.Version.Major < 2 || (m.Version.Major == 2 && m.Version.Minor < 29)
}

func (cp *Processor) approveDeleteContainer(e containerEvent.RemoveContainerRequest) {
err := cp.cnrClient.Morph().NotarySignAndInvokeTX(&e.MainTransaction, false)

Expand Down
Loading
Loading