diff --git a/session/common.go b/session/common.go new file mode 100644 index 00000000..0eb6254f --- /dev/null +++ b/session/common.go @@ -0,0 +1,334 @@ +package session + +import ( + "bytes" + "crypto/ecdsa" + "errors" + "fmt" + + "github.com/google/uuid" + "github.com/nspcc-dev/neofs-api-go/v2/refs" + "github.com/nspcc-dev/neofs-api-go/v2/session" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" + "github.com/nspcc-dev/neofs-sdk-go/user" +) + +type commonData struct { + idSet bool + id uuid.UUID + + issuerSet bool + issuer user.ID + + lifetimeSet bool + iat, nbf, exp uint64 + + authKey []byte + + sigSet bool + sig refs.Signature +} + +type contextReader func(session.TokenContext, bool) error + +// reads commonData and custom context from the session.Token message. +// If checkFieldPresence is set, returns an error on absence of any protocol-required +// field. Verifies format of any presented field according to NeoFS API V2 protocol. +// Calls contextReader if session context is set. Passes checkFieldPresence into contextReader. +func (x *commonData) readFromV2(m session.Token, checkFieldPresence bool, r contextReader) error { + var err error + + body := m.GetBody() + if checkFieldPresence && body == nil { + return errors.New("missing token body") + } + + binID := body.GetID() + if x.idSet = len(binID) > 0; x.idSet { + err = x.id.UnmarshalBinary(binID) + if err != nil { + return fmt.Errorf("invalid session ID: %w", err) + } else if ver := x.id.Version(); ver != 4 { + return fmt.Errorf("invalid session UUID version %d", ver) + } + } else if checkFieldPresence { + return errors.New("missing session ID") + } + + issuer := body.GetOwnerID() + if x.issuerSet = issuer != nil; x.issuerSet { + err = x.issuer.ReadFromV2(*issuer) + if err != nil { + return fmt.Errorf("invalid session issuer: %w", err) + } + } else if checkFieldPresence { + return errors.New("missing session issuer") + } + + lifetime := body.GetLifetime() + if x.lifetimeSet = lifetime != nil; x.lifetimeSet { + x.iat = lifetime.GetIat() + x.nbf = lifetime.GetNbf() + x.exp = lifetime.GetExp() + } else if checkFieldPresence { + return errors.New("missing token lifetime") + } + + x.authKey = body.GetSessionKey() + if checkFieldPresence && len(x.authKey) == 0 { + return errors.New("missing session public key") + } + + c := body.GetContext() + if c != nil { + err = r(c, checkFieldPresence) + if err != nil { + return fmt.Errorf("invalid context: %w", err) + } + } else if checkFieldPresence { + return errors.New("missing session context") + } + + sig := m.GetSignature() + if x.sigSet = sig != nil; sig != nil { + x.sig = *sig + } else if checkFieldPresence { + return errors.New("missing body signature") + } + + return nil +} + +type contextWriter func() session.TokenContext + +func (x commonData) fillBody(w contextWriter) *session.TokenBody { + var body session.TokenBody + + if x.idSet { + binID, err := x.id.MarshalBinary() + if err != nil { + panic(fmt.Sprintf("unexpected error from UUID.MarshalBinary: %v", err)) + } + + body.SetID(binID) + } + + if x.issuerSet { + var issuer refs.OwnerID + x.issuer.WriteToV2(&issuer) + + body.SetOwnerID(&issuer) + } + + if x.lifetimeSet { + var lifetime session.TokenLifetime + lifetime.SetIat(x.iat) + lifetime.SetNbf(x.nbf) + lifetime.SetExp(x.exp) + + body.SetLifetime(&lifetime) + } + + body.SetSessionKey(x.authKey) + + body.SetContext(w()) + + return &body +} + +func (x commonData) writeToV2(m *session.Token, w contextWriter) { + body := x.fillBody(w) + + m.SetBody(body) + + var sig *refs.Signature + + if x.sigSet { + sig = &x.sig + } + + m.SetSignature(sig) +} + +func (x commonData) signedData(w contextWriter) []byte { + return x.fillBody(w).StableMarshal(nil) +} + +func (x *commonData) sign(key ecdsa.PrivateKey, w contextWriter) error { + user.IDFromKey(&x.issuer, key.PublicKey) + x.issuerSet = true + + var sig neofscrypto.Signature + + err := sig.Calculate(neofsecdsa.Signer(key), x.signedData(w)) + if err != nil { + return err + } + + sig.WriteToV2(&x.sig) + x.sigSet = true + + return nil +} + +func (x commonData) verifySignature(w contextWriter) bool { + if !x.sigSet { + return false + } + + var sig neofscrypto.Signature + sig.ReadFromV2(x.sig) + + // TODO: (#233) check owner<->key relation + return sig.Verify(x.signedData(w)) +} + +func (x commonData) marshal(w contextWriter) []byte { + var m session.Token + x.writeToV2(&m, w) + + return m.StableMarshal(nil) +} + +func (x *commonData) unmarshal(data []byte, r contextReader) error { + var m session.Token + + err := m.Unmarshal(data) + if err != nil { + return err + } + + return x.readFromV2(m, false, r) +} + +func (x commonData) marshalJSON(w contextWriter) ([]byte, error) { + var m session.Token + x.writeToV2(&m, w) + + return m.MarshalJSON() +} + +func (x *commonData) unmarshalJSON(data []byte, r contextReader) error { + var m session.Token + + err := m.UnmarshalJSON(data) + if err != nil { + return err + } + + return x.readFromV2(m, false, r) +} + +// SetExp sets "exp" (expiration time) claim which identifies the expiration time +// (in NeoFS epochs) on or after which the session MUST NOT be accepted for +// processing. The processing of the "exp" claim requires that the current +// epoch MUST be before the expiration epoch listed in the "exp" claim. +// +// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4. +// +// See also ExpiredAt. +func (x *commonData) SetExp(exp uint64) { + x.exp = exp + x.lifetimeSet = true +} + +// SetNbf sets "nbf" (not before) claim which identifies the time (in NeoFS +// epochs) before which the session MUST NOT be accepted for processing. +// The processing of the "nbf" claim requires that the current date/time MUST be +// after or equal to the not-before date/time listed in the "nbf" claim. +// +// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5. +// +// See also InvalidAt. +func (x *commonData) SetNbf(nbf uint64) { + x.nbf = nbf + x.lifetimeSet = true +} + +// SetIat sets "iat" (issued at) claim which identifies the time (in NeoFS +// epochs) at which the session was issued. This claim can be used to +// determine the age of the session. +// +// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6. +// +// See also InvalidAt. +func (x *commonData) SetIat(iat uint64) { + x.iat = iat + x.lifetimeSet = true +} + +func (x commonData) expiredAt(epoch uint64) bool { + return !x.lifetimeSet || x.exp <= epoch +} + +// InvalidAt asserts "exp", "nbf" and "iat" claims. +// +// Zero session is invalid in any epoch. +// +// See also SetExp, SetNbf, SetIat. +func (x commonData) InvalidAt(epoch uint64) bool { + return x.expiredAt(epoch) || x.nbf > epoch || x.iat > epoch +} + +// SetID sets a unique identifier for the session. The identifier value MUST be +// assigned in a manner that ensures that there is a negligible probability +// that the same value will be accidentally assigned to a different session. +// +// ID format MUST be UUID version 4 (random). uuid.New can be used to generate +// a new ID. See https://datatracker.ietf.org/doc/html/rfc4122 and +// github.com/google/uuid package docs for details. +// +// See also ID. +func (x *commonData) SetID(id uuid.UUID) { + x.id = id + x.idSet = true +} + +// ID returns a unique identifier for the session. +// +// Zero session has empty UUID (all zeros, see uuid.Nil) which is legitimate +// but most likely not suitable. +// +// See also SetID. +func (x commonData) ID() uuid.UUID { + if x.idSet { + return x.id + } + + return uuid.Nil +} + +// SetAuthKey public key corresponding to the private key bound to the session. +// +// See also AssertAuthKey. +func (x *commonData) SetAuthKey(key neofscrypto.PublicKey) { + x.authKey = make([]byte, key.MaxEncodedSize()) + x.authKey = x.authKey[:key.Encode(x.authKey)] +} + +// AssertAuthKey asserts public key bound to the session. +// +// Zero session fails the check. +// +// See also SetAuthKey. +func (x commonData) AssertAuthKey(key neofscrypto.PublicKey) bool { + bKey := make([]byte, key.MaxEncodedSize()) + bKey = bKey[:key.Encode(bKey)] + + return bytes.Equal(bKey, x.authKey) +} + +// Issuer returns user ID of the session issuer. +// +// Makes sense only for signed session instances. For unsigned instances, +// Issuer returns zero user.ID. +// +// See also Sign. +func (x commonData) Issuer() user.ID { + if x.issuerSet { + return x.issuer + } + + return user.ID{} +} diff --git a/session/container.go b/session/container.go index b5722d26..2e08943f 100644 --- a/session/container.go +++ b/session/container.go @@ -1,17 +1,13 @@ package session import ( - "bytes" "crypto/ecdsa" "errors" "fmt" - "github.com/google/uuid" "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-api-go/v2/session" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" - neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" "github.com/nspcc-dev/neofs-sdk-go/user" ) @@ -26,67 +22,67 @@ import ( // // Instances can be created using built-in var declaration. type Container struct { + commonData + + verb ContainerVerb + cnrSet bool - - lt session.TokenLifetime - - c session.ContainerSessionContext - - body session.TokenBody - - sig neofscrypto.Signature + cnr cid.ID } -// ReadFromV2 reads Container from the session.Token message. +// readContext is a contextReader needed for commonData methods. +func (x *Container) readContext(c session.TokenContext, checkFieldPresence bool) error { + cCnr, ok := c.(*session.ContainerSessionContext) + if !ok || cCnr == nil { + return fmt.Errorf("invalid context %T", c) + } + + x.cnrSet = !cCnr.Wildcard() + cnr := cCnr.ContainerID() + + if x.cnrSet { + if cnr != nil { + err := x.cnr.ReadFromV2(*cnr) + if err != nil { + return fmt.Errorf("invalid container ID: %w", err) + } + } else if checkFieldPresence { + return errors.New("missing container or wildcard flag") + } + } else if cnr != nil { + return errors.New("container conflicts with wildcard flag") + } + + x.verb = ContainerVerb(cCnr.Verb()) + + return nil +} + +func (x *Container) readFromV2(m session.Token, checkFieldPresence bool) error { + return x.commonData.readFromV2(m, checkFieldPresence, x.readContext) +} + +// ReadFromV2 reads Container from the session.Token message. Checks if the +// message conforms to NeoFS API V2 protocol. // // See also WriteToV2. func (x *Container) ReadFromV2(m session.Token) error { - b := m.GetBody() - if b == nil { - return errors.New("missing body") + return x.readFromV2(m, true) +} + +func (x Container) writeContext() session.TokenContext { + var c session.ContainerSessionContext + c.SetWildcard(!x.cnrSet) + c.SetVerb(session.ContainerSessionVerb(x.verb)) + + if x.cnrSet { + var cnr refs.ContainerID + x.cnr.WriteToV2(&cnr) + + c.SetContainerID(&cnr) } - bID := b.GetID() - var id uuid.UUID - - err := id.UnmarshalBinary(bID) - if err != nil { - return fmt.Errorf("invalid binary ID: %w", err) - } else if ver := id.Version(); ver != 4 { - return fmt.Errorf("invalid UUID version %s", ver) - } - - c, ok := b.GetContext().(*session.ContainerSessionContext) - if !ok || c == nil { - return fmt.Errorf("invalid context %T", b.GetContext()) - } - - cnr := c.ContainerID() - x.cnrSet = !c.Wildcard() - - if x.cnrSet && cnr == nil { - return errors.New("container is not specified with unset wildcard") - } - - x.body = *b - - x.c = *c - - lt := b.GetLifetime() - if lt != nil { - x.lt = *lt - } else { - x.lt = session.TokenLifetime{} - } - - sig := m.GetSignature() - if sig != nil { - x.sig.ReadFromV2(*sig) - } else { - x.sig = neofscrypto.Signature{} - } - - return nil + return &c } // WriteToV2 writes Container to the session.Token message. @@ -94,11 +90,7 @@ func (x *Container) ReadFromV2(m session.Token) error { // // See also ReadFromV2. func (x Container) WriteToV2(m *session.Token) { - var sig refs.Signature - x.sig.WriteToV2(&sig) - - m.SetBody(&x.body) - m.SetSignature(&sig) + x.writeToV2(m, x.writeContext) } // Marshal encodes Container into a binary format of the NeoFS API protocol @@ -106,10 +98,7 @@ func (x Container) WriteToV2(m *session.Token) { // // See also Unmarshal. func (x Container) Marshal() []byte { - var m session.Token - x.WriteToV2(&m) - - return m.StableMarshal(nil) + return x.marshal(x.writeContext) } // Unmarshal decodes NeoFS API protocol binary format into the Container @@ -118,14 +107,7 @@ func (x Container) Marshal() []byte { // // See also Marshal. func (x *Container) Unmarshal(data []byte) error { - var m session.Token - - err := m.Unmarshal(data) - if err != nil { - return err - } - - return x.ReadFromV2(m) + return x.unmarshal(data, x.readContext) } // MarshalJSON encodes Container into a JSON format of the NeoFS API protocol @@ -133,10 +115,7 @@ func (x *Container) Unmarshal(data []byte) error { // // See also UnmarshalJSON. func (x Container) MarshalJSON() ([]byte, error) { - var m session.Token - x.WriteToV2(&m) - - return m.MarshalJSON() + return x.marshalJSON(x.writeContext) } // UnmarshalJSON decodes NeoFS API protocol JSON format into the Container @@ -144,14 +123,7 @@ func (x Container) MarshalJSON() ([]byte, error) { // // See also MarshalJSON. func (x *Container) UnmarshalJSON(data []byte) error { - var m session.Token - - err := m.UnmarshalJSON(data) - if err != nil { - return err - } - - return x.ReadFromV2(m) + return x.unmarshalJSON(data, x.readContext) } // Sign calculates and writes signature of the Container data. @@ -164,19 +136,7 @@ func (x *Container) UnmarshalJSON(data []byte) error { // // See also VerifySignature. func (x *Container) Sign(key ecdsa.PrivateKey) error { - var idUser user.ID - user.IDFromKey(&idUser, key.PublicKey) - - var idUserV2 refs.OwnerID - idUser.WriteToV2(&idUserV2) - - x.c.SetWildcard(!x.cnrSet) - - x.body.SetOwnerID(&idUserV2) - x.body.SetLifetime(&x.lt) - x.body.SetContext(&x.c) - - return x.sig.Calculate(neofsecdsa.Signer(key), x.body.StableMarshal(nil)) + return x.sign(key, x.writeContext) } // VerifySignature checks if Container signature is presented and valid. @@ -185,18 +145,14 @@ func (x *Container) Sign(key ecdsa.PrivateKey) error { // // See also Sign. func (x Container) VerifySignature() bool { - // TODO: (#233) check owner<->key relation - return x.sig.Verify(x.body.StableMarshal(nil)) + return x.verifySignature(x.writeContext) } // ApplyOnlyTo limits session scope to a given author container. // // See also AppliedTo. func (x *Container) ApplyOnlyTo(cnr cid.ID) { - var cnrv2 refs.ContainerID - cnr.WriteToV2(&cnrv2) - - x.c.SetContainerID(&cnrv2) + x.cnr = cnr x.cnrSet = true } @@ -206,18 +162,7 @@ func (x *Container) ApplyOnlyTo(cnr cid.ID) { // // See also ApplyOnlyTo. func (x Container) AppliedTo(cnr cid.ID) bool { - if !x.cnrSet { - return true - } - - var cnr2 cid.ID - - if err := cnr2.ReadFromV2(*x.c.ContainerID()); err != nil { - // NPE and error must never happen - panic(fmt.Sprintf("unexpected error from cid.ReadFromV2: %v", err)) - } - - return cnr2.Equals(cnr) + return !x.cnrSet || x.cnr.Equals(cnr) } // ContainerVerb enumerates container operations. @@ -236,7 +181,7 @@ const ( // // See also AssertVerb. func (x *Container) ForVerb(verb ContainerVerb) { - x.c.SetVerb(session.ContainerSessionVerb(verb)) + x.verb = verb } // AssertVerb checks if Container relates to the given container operation. @@ -245,134 +190,7 @@ func (x *Container) ForVerb(verb ContainerVerb) { // // See also ForVerb. func (x Container) AssertVerb(verb ContainerVerb) bool { - return verb == ContainerVerb(x.c.Verb()) -} - -// SetExp sets "exp" (expiration time) claim which identifies the expiration time -// (in NeoFS epochs) on or after which the Container MUST NOT be accepted for -// processing. The processing of the "exp" claim requires that the current -// epoch MUST be before the expiration epoch listed in the "exp" claim. -// -// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4. -// -// See also ExpiredAt. -func (x *Container) SetExp(exp uint64) { - x.lt.SetExp(exp) -} - -// ExpiredAt asserts "exp" claim. -// -// Zero Container is expired in any epoch. -// -// See also SetExp. -func (x Container) ExpiredAt(epoch uint64) bool { - return x.lt.GetExp() <= epoch -} - -// SetNbf sets "nbf" (not before) claim which identifies the time (in NeoFS -// epochs) before which the Container MUST NOT be accepted for processing. -// The processing of the "nbf" claim requires that the current date/time MUST be -// after or equal to the not-before date/time listed in the "nbf" claim. -// -// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5. -// -// See also InvalidAt. -func (x *Container) SetNbf(nbf uint64) { - x.lt.SetNbf(nbf) -} - -// SetIat sets "iat" (issued at) claim which identifies the time (in NeoFS -// epochs) at which the Container was issued. This claim can be used to -// determine the age of the Container. -// -// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6. -// -// See also InvalidAt. -func (x *Container) SetIat(iat uint64) { - x.lt.SetIat(iat) -} - -// InvalidAt asserts "exp", "nbf" and "iat" claims. -// -// Zero Container is invalid in any epoch. -// -// See also SetExp, SetNbf, SetIat. -func (x Container) InvalidAt(epoch uint64) bool { - return x.lt.GetNbf() > epoch || x.lt.GetIat() > epoch || x.ExpiredAt(epoch) -} - -// SetID sets a unique identifier for the session. The identifier value MUST be -// assigned in a manner that ensures that there is a negligible probability -// that the same value will be accidentally assigned to a different session. -// -// ID format MUST be UUID version 4 (random). uuid.New can be used to generate -// a new ID. See https://datatracker.ietf.org/doc/html/rfc4122 and -// github.com/google/uuid package docs for details. -// -// See also ID. -func (x *Container) SetID(id uuid.UUID) { - x.body.SetID(id[:]) -} - -// ID returns a unique identifier for the session. -// -// Zero Container has empty UUID (all zeros, see uuid.Nil) which is legitimate -// but most likely not suitable. -// -// See also SetID. -func (x Container) ID() uuid.UUID { - data := x.body.GetID() - if data == nil { - return uuid.Nil - } - - var id uuid.UUID - - err := id.UnmarshalBinary(x.body.GetID()) - if err != nil { - panic(fmt.Sprintf("unexpected error from UUID.UnmarshalBinary: %v", err)) - } - - return id -} - -// SetAuthKey public key corresponding to the private key bound to the session. -// -// See also AssertAuthKey. -func (x *Container) SetAuthKey(key neofscrypto.PublicKey) { - bKey := make([]byte, key.MaxEncodedSize()) - bKey = bKey[:key.Encode(bKey)] - - x.body.SetSessionKey(bKey) -} - -// AssertAuthKey asserts public key bound to the session. -// -// Zero Container fails the check. -// -// See also SetAuthKey. -func (x Container) AssertAuthKey(key neofscrypto.PublicKey) bool { - bKey := make([]byte, key.MaxEncodedSize()) - bKey = bKey[:key.Encode(bKey)] - - return bytes.Equal(bKey, x.body.GetSessionKey()) -} - -// Issuer returns user ID of the session issuer. -// -// Makes sense only for signed Container instances. For unsigned instances, -// Issuer returns zero user.ID. -// -// See also Sign. -func (x Container) Issuer() user.ID { - var issuer user.ID - - issuerV2 := x.body.GetOwnerID() - if issuerV2 != nil { - _ = issuer.ReadFromV2(*issuerV2) - } - - return issuer + return x.verb == verb } // IssuedBy checks if Container session is issued by the given user. diff --git a/session/container_test.go b/session/container_test.go index e084b9db..c35ba414 100644 --- a/session/container_test.go +++ b/session/container_test.go @@ -1,152 +1,382 @@ package session_test import ( + "fmt" "math" "math/rand" "testing" "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/util/slice" "github.com/nspcc-dev/neofs-api-go/v2/refs" v2session "github.com/nspcc-dev/neofs-api-go/v2/session" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" "github.com/nspcc-dev/neofs-sdk-go/session" sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" "github.com/nspcc-dev/neofs-sdk-go/user" + usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" ) -func TestContainer_ReadFromV2(t *testing.T) { - var x session.Container - var m v2session.Token - var b v2session.TokenBody - var c v2session.ContainerSessionContext +func TestContainerProtocolV2(t *testing.T) { + var validV2 v2session.Token + + var body v2session.TokenBody + validV2.SetBody(&body) + + // ID + id := uuid.New() + binID, err := id.MarshalBinary() + require.NoError(t, err) + restoreID := func() { + body.SetID(binID) + } + restoreID() + + // Owner + usr := *usertest.ID() + var usrV2 refs.OwnerID + usr.WriteToV2(&usrV2) + restoreUser := func() { + body.SetOwnerID(&usrV2) + } + restoreUser() + + // Lifetime + var lifetime v2session.TokenLifetime + lifetime.SetIat(1) + lifetime.SetNbf(2) + lifetime.SetExp(3) + restoreLifetime := func() { + body.SetLifetime(&lifetime) + } + restoreLifetime() + + // Session key + signer := randSigner() + authKey := neofsecdsa.PublicKey(signer.PublicKey) + binAuthKey := make([]byte, authKey.MaxEncodedSize()) + binAuthKey = binAuthKey[:authKey.Encode(binAuthKey)] + restoreAuthKey := func() { + body.SetSessionKey(binAuthKey) + } + restoreAuthKey() + + // Context + cnr := cidtest.ID() + var cnrV2 refs.ContainerID + cnr.WriteToV2(&cnrV2) + var cCnr v2session.ContainerSessionContext + restoreCtx := func() { + cCnr.SetContainerID(&cnrV2) + cCnr.SetWildcard(false) + body.SetContext(&cCnr) + } + restoreCtx() + + // Signature + var sig refs.Signature + restoreSig := func() { + validV2.SetSignature(&sig) + } + restoreSig() + + // TODO(@cthulhu-rider): #260 use functionality for message corruption + + for _, testcase := range []struct { + name string + corrupt []func() + restore func() + assert func(session.Container) + breakSign func(*v2session.Token) + }{ + { + name: "Signature", + corrupt: []func(){ + func() { + validV2.SetSignature(nil) + }, + }, + restore: restoreSig, + }, + { + name: "ID", + corrupt: []func(){ + func() { + body.SetID([]byte{1, 2, 3}) + }, + func() { + id, err := uuid.NewDCEPerson() + require.NoError(t, err) + bindID, err := id.MarshalBinary() + require.NoError(t, err) + body.SetID(bindID) + }, + }, + restore: restoreID, + assert: func(val session.Container) { + require.Equal(t, id, val.ID()) + }, + breakSign: func(m *v2session.Token) { + id := m.GetBody().GetID() + id[len(id)-1]++ + }, + }, + { + name: "User", + corrupt: []func(){ + func() { + var brokenUsrV2 refs.OwnerID + brokenUsrV2.SetValue(append(usrV2.GetValue(), 1)) + body.SetOwnerID(&brokenUsrV2) + }, + }, + restore: restoreUser, + assert: func(val session.Container) { + require.Equal(t, usr, val.Issuer()) + }, + breakSign: func(m *v2session.Token) { + id := m.GetBody().GetOwnerID().GetValue() + copy(id, usertest.ID().WalletBytes()) + }, + }, + { + name: "Lifetime", + corrupt: []func(){ + func() { + body.SetLifetime(nil) + }, + }, + restore: restoreLifetime, + assert: func(val session.Container) { + require.True(t, val.InvalidAt(1)) + require.False(t, val.InvalidAt(2)) + require.True(t, val.InvalidAt(3)) + }, + breakSign: func(m *v2session.Token) { + lt := m.GetBody().GetLifetime() + lt.SetIat(lt.GetIat() + 1) + }, + }, + { + name: "Auth key", + corrupt: []func(){ + func() { + body.SetSessionKey(nil) + }, + func() { + body.SetSessionKey([]byte{}) + }, + }, + restore: restoreAuthKey, + assert: func(val session.Container) { + require.True(t, val.AssertAuthKey(&authKey)) + }, + breakSign: func(m *v2session.Token) { + body := m.GetBody() + key := body.GetSessionKey() + cp := slice.Copy(key) + cp[len(cp)-1]++ + body.SetSessionKey(cp) + }, + }, + { + name: "Context", + corrupt: []func(){ + func() { + body.SetContext(nil) + }, + func() { + cCnr.SetWildcard(true) + }, + func() { + cCnr.SetContainerID(nil) + }, + func() { + var brokenCnr refs.ContainerID + brokenCnr.SetValue(append(cnrV2.GetValue(), 1)) + cCnr.SetContainerID(&brokenCnr) + }, + }, + restore: restoreCtx, + assert: func(val session.Container) { + require.True(t, val.AppliedTo(cnr)) + require.False(t, val.AppliedTo(cidtest.ID())) + }, + breakSign: func(m *v2session.Token) { + cnr := m.GetBody().GetContext().(*v2session.ContainerSessionContext).ContainerID().GetValue() + cnr[len(cnr)-1]++ + }, + }, + } { + var val session.Container + + for i, corrupt := range testcase.corrupt { + corrupt() + require.Error(t, val.ReadFromV2(validV2), testcase.name, fmt.Sprintf("corrupt #%d", i)) + + testcase.restore() + require.NoError(t, val.ReadFromV2(validV2), testcase.name) + + if testcase.assert != nil { + testcase.assert(val) + } + + if testcase.breakSign != nil { + require.NoError(t, val.Sign(signer), testcase.name) + require.True(t, val.VerifySignature(), testcase.name) + + var signedV2 v2session.Token + val.WriteToV2(&signedV2) + + var restored session.Container + require.NoError(t, restored.ReadFromV2(signedV2), testcase.name) + require.True(t, restored.VerifySignature(), testcase.name) + + testcase.breakSign(&signedV2) + + require.NoError(t, restored.ReadFromV2(signedV2), testcase.name) + require.False(t, restored.VerifySignature(), testcase.name) + } + } + } +} + +func TestContainer_WriteToV2(t *testing.T) { + var val session.Container + + assert := func(baseAssert func(v2session.Token)) { + var m v2session.Token + val.WriteToV2(&m) + baseAssert(m) + } + + // ID id := uuid.New() - t.Run("protocol violation", func(t *testing.T) { - require.Error(t, x.ReadFromV2(m)) + binID, err := id.MarshalBinary() + require.NoError(t, err) - m.SetBody(&b) - - require.Error(t, x.ReadFromV2(m)) - - b.SetID(id[:]) - - require.Error(t, x.ReadFromV2(m)) - - b.SetContext(&c) - - require.Error(t, x.ReadFromV2(m)) - - c.SetWildcard(true) - - require.NoError(t, x.ReadFromV2(m)) + val.SetID(id) + assert(func(m v2session.Token) { + require.Equal(t, binID, m.GetBody().GetID()) }) - m.SetBody(&b) - b.SetContext(&c) - b.SetID(id[:]) - c.SetWildcard(true) + // Owner/Signature + signer := randSigner() - t.Run("container", func(t *testing.T) { - cnr1 := cidtest.ID() - cnr2 := cidtest.ID() + require.NoError(t, val.Sign(signer)) - require.NoError(t, x.ReadFromV2(m)) - require.True(t, x.AppliedTo(cnr1)) - require.True(t, x.AppliedTo(cnr2)) + var usr user.ID + user.IDFromKey(&usr, signer.PublicKey) - var cnrv2 refs.ContainerID - cnr1.WriteToV2(&cnrv2) + var usrV2 refs.OwnerID + usr.WriteToV2(&usrV2) - c.SetContainerID(&cnrv2) - c.SetWildcard(false) + assert(func(m v2session.Token) { + require.Equal(t, &usrV2, m.GetBody().GetOwnerID()) - require.NoError(t, x.ReadFromV2(m)) - require.True(t, x.AppliedTo(cnr1)) - require.False(t, x.AppliedTo(cnr2)) + sig := m.GetSignature() + require.NotZero(t, sig.GetKey()) + require.NotZero(t, sig.GetSign()) }) - t.Run("verb", func(t *testing.T) { - require.NoError(t, x.ReadFromV2(m)) - require.True(t, x.AssertVerb(0)) + // Lifetime + const iat, nbf, exp = 1, 2, 3 + val.SetIat(iat) + val.SetNbf(nbf) + val.SetExp(exp) - verb := v2session.ContainerSessionVerb(rand.Uint32()) - - c.SetVerb(verb) - - require.NoError(t, x.ReadFromV2(m)) - require.True(t, x.AssertVerb(session.ContainerVerb(verb))) + assert(func(m v2session.Token) { + lt := m.GetBody().GetLifetime() + require.EqualValues(t, iat, lt.GetIat()) + require.EqualValues(t, nbf, lt.GetNbf()) + require.EqualValues(t, exp, lt.GetExp()) }) - t.Run("id", func(t *testing.T) { - id := uuid.New() - bID := id[:] - - b.SetID(bID) - - require.NoError(t, x.ReadFromV2(m)) - require.Equal(t, id, x.ID()) + // Context + assert(func(m v2session.Token) { + cCnr, ok := m.GetBody().GetContext().(*v2session.ContainerSessionContext) + require.True(t, ok) + require.True(t, cCnr.Wildcard()) + require.Zero(t, cCnr.ContainerID()) }) - t.Run("lifetime", func(t *testing.T) { - const nbf, iat, exp = 11, 22, 33 + cnr := cidtest.ID() - var lt v2session.TokenLifetime - lt.SetNbf(nbf) - lt.SetIat(iat) - lt.SetExp(exp) + var cnrV2 refs.ContainerID + cnr.WriteToV2(&cnrV2) - b.SetLifetime(<) + val.ApplyOnlyTo(cnr) - require.NoError(t, x.ReadFromV2(m)) - require.False(t, x.ExpiredAt(exp-1)) - require.True(t, x.ExpiredAt(exp)) - require.True(t, x.ExpiredAt(exp+1)) - require.True(t, x.InvalidAt(nbf-1)) - require.True(t, x.InvalidAt(iat-1)) - require.False(t, x.InvalidAt(iat)) - require.False(t, x.InvalidAt(exp-1)) - require.True(t, x.InvalidAt(exp)) - require.True(t, x.InvalidAt(exp+1)) - }) - - t.Run("session key", func(t *testing.T) { - key := randPublicKey() - - bKey := make([]byte, key.MaxEncodedSize()) - bKey = bKey[:key.Encode(bKey)] - - b.SetSessionKey(bKey) - - require.NoError(t, x.ReadFromV2(m)) - require.True(t, x.AssertAuthKey(key)) + assert(func(m v2session.Token) { + cCnr, ok := m.GetBody().GetContext().(*v2session.ContainerSessionContext) + require.True(t, ok) + require.False(t, cCnr.Wildcard()) + require.Equal(t, &cnrV2, cCnr.ContainerID()) }) } -func TestEncodingContainer(t *testing.T) { - tok := *sessiontest.ContainerSigned() +func TestContainer_ApplyOnlyTo(t *testing.T) { + var val session.Container + var m v2session.Token + filled := sessiontest.Container() - t.Run("binary", func(t *testing.T) { - data := tok.Marshal() + assertDefaults := func() { + cCnr, ok := m.GetBody().GetContext().(*v2session.ContainerSessionContext) + require.True(t, ok) + require.True(t, cCnr.Wildcard()) + require.Zero(t, cCnr.ContainerID()) + } - var tok2 session.Container - require.NoError(t, tok2.Unmarshal(data)) + assertBinary := func(baseAssert func()) { + val2 := filled - require.Equal(t, tok, tok2) - }) + require.NoError(t, val2.Unmarshal(val.Marshal())) + baseAssert() + } - t.Run("json", func(t *testing.T) { - data, err := tok.MarshalJSON() + assertJSON := func(baseAssert func()) { + val2 := filled + + jd, err := val.MarshalJSON() require.NoError(t, err) - var tok2 session.Container - require.NoError(t, tok2.UnmarshalJSON(data)) + require.NoError(t, val2.UnmarshalJSON(jd)) + baseAssert() + } - require.Equal(t, tok, tok2) - }) + val.WriteToV2(&m) + + assertDefaults() + assertBinary(assertDefaults) + assertJSON(assertDefaults) + + // set value + + cnr := cidtest.ID() + + var cnrV2 refs.ContainerID + cnr.WriteToV2(&cnrV2) + + val.ApplyOnlyTo(cnr) + + val.WriteToV2(&m) + + assertCnr := func() { + cCnr, ok := m.GetBody().GetContext().(*v2session.ContainerSessionContext) + require.True(t, ok) + require.False(t, cCnr.Wildcard()) + require.Equal(t, &cnrV2, cCnr.ContainerID()) + } + + assertCnr() + assertBinary(assertCnr) + assertJSON(assertCnr) } -func TestContainerAppliedTo(t *testing.T) { +func TestContainer_AppliedTo(t *testing.T) { var x session.Container cnr1 := cidtest.ID() @@ -161,21 +391,7 @@ func TestContainerAppliedTo(t *testing.T) { require.False(t, x.AppliedTo(cnr2)) } -func TestContainerExp(t *testing.T) { - var x session.Container - - exp := rand.Uint64() - - require.True(t, x.ExpiredAt(exp)) - - x.SetExp(exp) - - require.False(t, x.ExpiredAt(exp-1)) - require.True(t, x.ExpiredAt(exp)) - require.True(t, x.ExpiredAt(exp+1)) -} - -func TestContainerLifetime(t *testing.T) { +func TestContainer_InvalidAt(t *testing.T) { var x session.Container nbf := rand.Uint64() @@ -196,7 +412,7 @@ func TestContainerLifetime(t *testing.T) { require.True(t, x.InvalidAt(exp)) } -func TestContainerID(t *testing.T) { +func TestContainer_ID(t *testing.T) { var x session.Container require.Zero(t, x.ID()) @@ -208,7 +424,7 @@ func TestContainerID(t *testing.T) { require.Equal(t, id, x.ID()) } -func TestContainerAuthKey(t *testing.T) { +func TestContainer_AssertAuthKey(t *testing.T) { var x session.Container key := randPublicKey() @@ -220,7 +436,64 @@ func TestContainerAuthKey(t *testing.T) { require.True(t, x.AssertAuthKey(key)) } -func TestContainerVerb(t *testing.T) { +func TestContainer_ForVerb(t *testing.T) { + var val session.Container + var m v2session.Token + filled := sessiontest.Container() + + assertDefaults := func() { + cCnr, ok := m.GetBody().GetContext().(*v2session.ContainerSessionContext) + require.True(t, ok) + require.Zero(t, cCnr.Verb()) + } + + assertBinary := func(baseAssert func()) { + val2 := filled + + require.NoError(t, val2.Unmarshal(val.Marshal())) + baseAssert() + } + + assertJSON := func(baseAssert func()) { + val2 := filled + + jd, err := val.MarshalJSON() + require.NoError(t, err) + + require.NoError(t, val2.UnmarshalJSON(jd)) + baseAssert() + } + + val.WriteToV2(&m) + + assertDefaults() + assertBinary(assertDefaults) + assertJSON(assertDefaults) + + // set value + + assertVerb := func(verb v2session.ContainerSessionVerb) { + cCnr, ok := m.GetBody().GetContext().(*v2session.ContainerSessionContext) + require.True(t, ok) + require.Equal(t, verb, cCnr.Verb()) + } + + for from, to := range map[session.ContainerVerb]v2session.ContainerSessionVerb{ + session.VerbContainerPut: v2session.ContainerVerbPut, + session.VerbContainerDelete: v2session.ContainerVerbDelete, + session.VerbContainerSetEACL: v2session.ContainerVerbSetEACL, + } { + val.ForVerb(from) + + val.WriteToV2(&m) + + assertVerb(to) + assertBinary(func() { assertVerb(to) }) + assertJSON(func() { assertVerb(to) }) + } +} + +func TestContainer_AssertVerb(t *testing.T) { var x session.Container const v1, v2 = session.VerbContainerPut, session.VerbContainerDelete @@ -233,60 +506,6 @@ func TestContainerVerb(t *testing.T) { require.False(t, x.AssertVerb(v2)) } -func TestContainerSignature(t *testing.T) { - var x session.Container - - const nbf = 11 - const iat = 22 - const exp = 33 - id := uuid.New() - key := randPublicKey() - cnr := cidtest.ID() - verb := session.VerbContainerPut - - signer := randSigner() - - fs := []func(){ - func() { x.SetNbf(nbf) }, - func() { x.SetNbf(nbf + 1) }, - - func() { x.SetIat(iat) }, - func() { x.SetIat(iat + 1) }, - - func() { x.SetExp(exp) }, - func() { x.SetExp(exp + 1) }, - - func() { x.SetID(id) }, - func() { - idcp := id - idcp[0]++ - x.SetID(idcp) - }, - - func() { x.SetAuthKey(key) }, - func() { x.SetAuthKey(randPublicKey()) }, - - func() { x.ApplyOnlyTo(cnr) }, - func() { x.ApplyOnlyTo(cidtest.ID()) }, - - func() { x.ForVerb(verb) }, - func() { x.ForVerb(verb + 1) }, - } - - for i := 0; i < len(fs); i += 2 { - fs[i]() - - require.NoError(t, x.Sign(signer)) - require.True(t, x.VerifySignature()) - - fs[i+1]() - require.False(t, x.VerifySignature()) - - fs[i]() - require.True(t, x.VerifySignature()) - } -} - func TestIssuedBy(t *testing.T) { var ( token session.Container diff --git a/session/object.go b/session/object.go index 7b7f6dd3..8cd18138 100644 --- a/session/object.go +++ b/session/object.go @@ -1,19 +1,14 @@ package session import ( - "bytes" "crypto/ecdsa" "errors" "fmt" - "github.com/google/uuid" "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-api-go/v2/session" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" - neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" - "github.com/nspcc-dev/neofs-sdk-go/user" ) // Object represents token of the NeoFS Object session. A session is opened @@ -27,69 +22,90 @@ import ( // // Instances can be created using built-in var declaration. type Object struct { - lt session.TokenLifetime + commonData - obj refs.Address + verb ObjectVerb - c session.ObjectSessionContext + cnrSet bool + cnr cid.ID - body session.TokenBody - - sig neofscrypto.Signature + objSet bool + obj oid.ID } -// ReadFromV2 reads Object from the session.Token message. +func (x *Object) readContext(c session.TokenContext, checkFieldPresence bool) error { + cObj, ok := c.(*session.ObjectSessionContext) + if !ok || cObj == nil { + return fmt.Errorf("invalid context %T", c) + } + + addr := cObj.GetAddress() + if checkFieldPresence && addr == nil { + return errors.New("missing object address") + } + + var err error + + cnr := addr.GetContainerID() + if x.cnrSet = cnr != nil; x.cnrSet { + err := x.cnr.ReadFromV2(*cnr) + if err != nil { + return fmt.Errorf("invalid container ID: %w", err) + } + } else if checkFieldPresence { + return errors.New("missing container in object address") + } + + obj := addr.GetObjectID() + if x.objSet = obj != nil; x.objSet { + err = x.obj.ReadFromV2(*obj) + if err != nil { + return fmt.Errorf("invalid object ID: %w", err) + } + } + + x.verb = ObjectVerb(cObj.GetVerb()) + + return nil +} + +func (x *Object) readFromV2(m session.Token, checkFieldPresence bool) error { + return x.commonData.readFromV2(m, checkFieldPresence, x.readContext) +} + +// ReadFromV2 reads Object from the session.Token message. Checks if the +// message conforms to NeoFS API V2 protocol. // // See also WriteToV2. func (x *Object) ReadFromV2(m session.Token) error { - b := m.GetBody() - if b == nil { - return errors.New("missing body") + return x.readFromV2(m, true) +} + +func (x Object) writeContext() session.TokenContext { + var c session.ObjectSessionContext + c.SetVerb(session.ObjectSessionVerb(x.verb)) + + if x.cnrSet || x.objSet { + var addr refs.Address + + if x.cnrSet { + var cnr refs.ContainerID + x.cnr.WriteToV2(&cnr) + + addr.SetContainerID(&cnr) + } + + if x.objSet { + var obj refs.ObjectID + x.obj.WriteToV2(&obj) + + addr.SetObjectID(&obj) + } + + c.SetAddress(&addr) } - bID := b.GetID() - var id uuid.UUID - - err := id.UnmarshalBinary(bID) - if err != nil { - return fmt.Errorf("invalid binary ID: %w", err) - } else if ver := id.Version(); ver != 4 { - return fmt.Errorf("invalid UUID version %s", ver) - } - - c, ok := b.GetContext().(*session.ObjectSessionContext) - if !ok || c == nil { - return fmt.Errorf("invalid context %T", b.GetContext()) - } - - x.body = *b - - x.c = *c - - obj := c.GetAddress() - - cnr := obj.GetContainerID() - if cnr == nil { - return errors.New("missing bound container") - } - - x.obj = *obj - - lt := b.GetLifetime() - if lt != nil { - x.lt = *lt - } else { - x.lt = session.TokenLifetime{} - } - - sig := m.GetSignature() - if sig != nil { - x.sig.ReadFromV2(*sig) - } else { - x.sig = neofscrypto.Signature{} - } - - return nil + return &c } // WriteToV2 writes Object to the session.Token message. @@ -97,11 +113,7 @@ func (x *Object) ReadFromV2(m session.Token) error { // // See also ReadFromV2. func (x Object) WriteToV2(m *session.Token) { - var sig refs.Signature - x.sig.WriteToV2(&sig) - - m.SetBody(&x.body) - m.SetSignature(&sig) + x.writeToV2(m, x.writeContext) } // Marshal encodes Object into a binary format of the NeoFS API protocol @@ -112,7 +124,7 @@ func (x Object) Marshal() []byte { var m session.Token x.WriteToV2(&m) - return m.StableMarshal(nil) + return x.marshal(x.writeContext) } // Unmarshal decodes NeoFS API protocol binary format into the Object @@ -121,14 +133,7 @@ func (x Object) Marshal() []byte { // // See also Marshal. func (x *Object) Unmarshal(data []byte) error { - var m session.Token - - err := m.Unmarshal(data) - if err != nil { - return err - } - - return x.ReadFromV2(m) + return x.unmarshal(data, x.readContext) } // MarshalJSON encodes Object into a JSON format of the NeoFS API protocol @@ -136,10 +141,7 @@ func (x *Object) Unmarshal(data []byte) error { // // See also UnmarshalJSON. func (x Object) MarshalJSON() ([]byte, error) { - var m session.Token - x.WriteToV2(&m) - - return m.MarshalJSON() + return x.marshalJSON(x.writeContext) } // UnmarshalJSON decodes NeoFS API protocol JSON format into the Object @@ -147,14 +149,7 @@ func (x Object) MarshalJSON() ([]byte, error) { // // See also MarshalJSON. func (x *Object) UnmarshalJSON(data []byte) error { - var m session.Token - - err := m.UnmarshalJSON(data) - if err != nil { - return err - } - - return x.ReadFromV2(m) + return x.unmarshalJSON(data, x.readContext) } // Sign calculates and writes signature of the Object data. @@ -167,19 +162,7 @@ func (x *Object) UnmarshalJSON(data []byte) error { // // See also VerifySignature. func (x *Object) Sign(key ecdsa.PrivateKey) error { - var idUser user.ID - user.IDFromKey(&idUser, key.PublicKey) - - var idUserV2 refs.OwnerID - idUser.WriteToV2(&idUserV2) - - x.c.SetAddress(&x.obj) - - x.body.SetOwnerID(&idUserV2) - x.body.SetLifetime(&x.lt) - x.body.SetContext(&x.c) - - return x.sig.Calculate(neofsecdsa.Signer(key), x.body.StableMarshal(nil)) + return x.sign(key, x.writeContext) } // VerifySignature checks if Object signature is presented and valid. @@ -189,7 +172,7 @@ func (x *Object) Sign(key ecdsa.PrivateKey) error { // See also Sign. func (x Object) VerifySignature() bool { // TODO: (#233) check owner<->key relation - return x.sig.Verify(x.body.StableMarshal(nil)) + return x.verifySignature(x.writeContext) } // BindContainer binds the Object session to a given container. Each session @@ -197,10 +180,8 @@ func (x Object) VerifySignature() bool { // // See also AssertContainer. func (x *Object) BindContainer(cnr cid.ID) { - var cnrV2 refs.ContainerID - cnr.WriteToV2(&cnrV2) - - x.obj.SetContainerID(&cnrV2) + x.cnr = cnr + x.cnrSet = true } // AssertContainer checks if Object session bound to a given container. @@ -210,16 +191,7 @@ func (x *Object) BindContainer(cnr cid.ID) { // // See also BindContainer. func (x Object) AssertContainer(cnr cid.ID) bool { - cnrV2 := x.obj.GetContainerID() - if cnrV2 == nil { - return false - } - - var cnr2 cid.ID - - err := cnr2.ReadFromV2(*cnrV2) - - return err == nil && cnr2.Equals(cnr) + return x.cnr.Equals(cnr) } // LimitByObject limits session scope to a given object from the container @@ -227,10 +199,8 @@ func (x Object) AssertContainer(cnr cid.ID) bool { // // See also AssertObject. func (x *Object) LimitByObject(obj oid.ID) { - var objV2 refs.ObjectID - obj.WriteToV2(&objV2) - - x.obj.SetObjectID(&objV2) + x.obj = obj + x.objSet = true } // AssertObject checks if Object session is applied to a given object. @@ -239,16 +209,7 @@ func (x *Object) LimitByObject(obj oid.ID) { // // See also LimitByObject. func (x Object) AssertObject(obj oid.ID) bool { - objV2 := x.obj.GetObjectID() - if objV2 == nil { - return true - } - - var obj2 oid.ID - - err := obj2.ReadFromV2(*objV2) - - return err == nil && obj2.Equals(obj) + return !x.objSet || x.obj.Equals(obj) } // ObjectVerb enumerates object operations. @@ -271,7 +232,7 @@ const ( // // See also AssertVerb. func (x *Object) ForVerb(verb ObjectVerb) { - x.c.SetVerb(session.ObjectSessionVerb(verb)) + x.verb = verb } // AssertVerb checks if Object relates to one of the given object operations. @@ -280,10 +241,8 @@ func (x *Object) ForVerb(verb ObjectVerb) { // // See also ForVerb. func (x Object) AssertVerb(verbs ...ObjectVerb) bool { - verb := ObjectVerb(x.c.GetVerb()) - for i := range verbs { - if verbs[i] == verb { + if verbs[i] == x.verb { return true } } @@ -291,129 +250,11 @@ func (x Object) AssertVerb(verbs ...ObjectVerb) bool { return false } -// SetExp sets "exp" (expiration time) claim which identifies the expiration time -// (in NeoFS epochs) on or after which the Object MUST NOT be accepted for -// processing. The processing of the "exp" claim requires that the current -// epoch MUST be before the expiration epoch listed in the "exp" claim. -// -// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4. -// -// See also ExpiredAt. -func (x *Object) SetExp(exp uint64) { - x.lt.SetExp(exp) -} - // ExpiredAt asserts "exp" claim. // // Zero Object is expired in any epoch. // // See also SetExp. func (x Object) ExpiredAt(epoch uint64) bool { - return x.lt.GetExp() <= epoch -} - -// SetNbf sets "nbf" (not before) claim which identifies the time (in NeoFS -// epochs) before which the Object MUST NOT be accepted for processing. -// The processing of the "nbf" claim requires that the current date/time MUST be -// after or equal to the not-before date/time listed in the "nbf" claim. -// -// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5. -// -// See also InvalidAt. -func (x *Object) SetNbf(nbf uint64) { - x.lt.SetNbf(nbf) -} - -// SetIat sets "iat" (issued at) claim which identifies the time (in NeoFS -// epochs) at which the Object was issued. This claim can be used to -// determine the age of the Object. -// -// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6. -// -// See also InvalidAt. -func (x *Object) SetIat(iat uint64) { - x.lt.SetIat(iat) -} - -// InvalidAt asserts "exp", "nbf" and "iat" claims. -// -// Zero Object is invalid in any epoch. -// -// See also SetExp, SetNbf, SetIat. -func (x Object) InvalidAt(epoch uint64) bool { - return x.lt.GetNbf() > epoch || x.lt.GetIat() > epoch || x.ExpiredAt(epoch) -} - -// SetID sets a unique identifier for the session. The identifier value MUST be -// assigned in a manner that ensures that there is a negligible probability -// that the same value will be accidentally assigned to a different session. -// -// ID format MUST be UUID version 4 (random). uuid.New can be used to generate -// a new ID. See https://datatracker.ietf.org/doc/html/rfc4122 and -// github.com/google/uuid package docs for details. -// -// See also ID. -func (x *Object) SetID(id uuid.UUID) { - x.body.SetID(id[:]) -} - -// ID returns a unique identifier for the session. -// -// Zero Object has empty UUID (all zeros, see uuid.Nil) which is legitimate -// but most likely not suitable. -// -// See also SetID. -func (x Object) ID() uuid.UUID { - data := x.body.GetID() - if data == nil { - return uuid.Nil - } - - var id uuid.UUID - - err := id.UnmarshalBinary(x.body.GetID()) - if err != nil { - panic(fmt.Sprintf("unexpected error from UUID.UnmarshalBinary: %v", err)) - } - - return id -} - -// SetAuthKey public key corresponding to the private key bound to the session. -// -// See also AssertAuthKey. -func (x *Object) SetAuthKey(key neofscrypto.PublicKey) { - bKey := make([]byte, key.MaxEncodedSize()) - bKey = bKey[:key.Encode(bKey)] - - x.body.SetSessionKey(bKey) -} - -// AssertAuthKey asserts public key bound to the session. -// -// Zero Object fails the check. -// -// See also SetAuthKey. -func (x Object) AssertAuthKey(key neofscrypto.PublicKey) bool { - bKey := make([]byte, key.MaxEncodedSize()) - bKey = bKey[:key.Encode(bKey)] - - return bytes.Equal(bKey, x.body.GetSessionKey()) -} - -// Issuer returns user ID of the session issuer. -// -// Makes sense only for signed Object instances. For unsigned instances, -// Issuer returns zero user.ID. -// -// See also Sign. -func (x Object) Issuer() user.ID { - var issuer user.ID - - issuerV2 := x.body.GetOwnerID() - if issuerV2 != nil { - _ = issuer.ReadFromV2(*issuerV2) - } - - return issuer + return x.expiredAt(epoch) } diff --git a/session/object_test.go b/session/object_test.go index 00cc0a8a..5c7360c1 100644 --- a/session/object_test.go +++ b/session/object_test.go @@ -9,15 +9,18 @@ import ( "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/util/slice" "github.com/nspcc-dev/neofs-api-go/v2/refs" v2session "github.com/nspcc-dev/neofs-api-go/v2/session" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" "github.com/nspcc-dev/neofs-sdk-go/session" sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" "github.com/nspcc-dev/neofs-sdk-go/user" + usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" ) @@ -35,143 +38,378 @@ func randPublicKey() neofscrypto.PublicKey { return (*neofsecdsa.PublicKey)(&k) } -func TestObject_ReadFromV2(t *testing.T) { - var x session.Object - var m v2session.Token - var b v2session.TokenBody - var c v2session.ObjectSessionContext +func TestObjectProtocolV2(t *testing.T) { + var validV2 v2session.Token + + var body v2session.TokenBody + validV2.SetBody(&body) + + // ID id := uuid.New() + binID, err := id.MarshalBinary() + require.NoError(t, err) + restoreID := func() { + body.SetID(binID) + } + restoreID() + + // Owner + usr := *usertest.ID() + var usrV2 refs.OwnerID + usr.WriteToV2(&usrV2) + restoreUser := func() { + body.SetOwnerID(&usrV2) + } + restoreUser() + + // Lifetime + var lifetime v2session.TokenLifetime + lifetime.SetIat(1) + lifetime.SetNbf(2) + lifetime.SetExp(3) + restoreLifetime := func() { + body.SetLifetime(&lifetime) + } + restoreLifetime() + + // Session key + signer := randSigner() + authKey := neofsecdsa.PublicKey(signer.PublicKey) + binAuthKey := make([]byte, authKey.MaxEncodedSize()) + binAuthKey = binAuthKey[:authKey.Encode(binAuthKey)] + restoreAuthKey := func() { + body.SetSessionKey(binAuthKey) + } + restoreAuthKey() + + // Context + cnr := cidtest.ID() + obj := oidtest.ID() + var addr oid.Address + addr.SetContainer(cnr) + addr.SetObject(obj) + var addrV2 refs.Address + addr.WriteToV2(&addrV2) + var cObj v2session.ObjectSessionContext + restoreCtx := func() { + cObj.SetAddress(&addrV2) + body.SetContext(&cObj) + } + restoreCtx() + + // Signature + var sig refs.Signature + restoreSig := func() { + validV2.SetSignature(&sig) + } + restoreSig() + + // TODO(@cthulhu-rider): #260 use functionality for message corruption + + for _, testcase := range []struct { + name string + corrupt []func() + restore func() + assert func(session.Object) + breakSign func(*v2session.Token) + }{ + { + name: "Signature", + corrupt: []func(){ + func() { + validV2.SetSignature(nil) + }, + }, + restore: restoreSig, + }, + { + name: "ID", + corrupt: []func(){ + func() { + body.SetID([]byte{1, 2, 3}) + }, + func() { + id, err := uuid.NewDCEPerson() + require.NoError(t, err) + bindID, err := id.MarshalBinary() + require.NoError(t, err) + body.SetID(bindID) + }, + }, + restore: restoreID, + assert: func(val session.Object) { + require.Equal(t, id, val.ID()) + }, + breakSign: func(m *v2session.Token) { + id := m.GetBody().GetID() + id[len(id)-1]++ + }, + }, + { + name: "User", + corrupt: []func(){ + func() { + var brokenUsrV2 refs.OwnerID + brokenUsrV2.SetValue(append(usrV2.GetValue(), 1)) + body.SetOwnerID(&brokenUsrV2) + }, + }, + restore: restoreUser, + assert: func(val session.Object) { + require.Equal(t, usr, val.Issuer()) + }, + breakSign: func(m *v2session.Token) { + id := m.GetBody().GetOwnerID().GetValue() + copy(id, usertest.ID().WalletBytes()) + }, + }, + { + name: "Lifetime", + corrupt: []func(){ + func() { + body.SetLifetime(nil) + }, + }, + restore: restoreLifetime, + assert: func(val session.Object) { + require.True(t, val.InvalidAt(1)) + require.False(t, val.InvalidAt(2)) + require.True(t, val.InvalidAt(3)) + }, + breakSign: func(m *v2session.Token) { + lt := m.GetBody().GetLifetime() + lt.SetIat(lt.GetIat() + 1) + }, + }, + { + name: "Auth key", + corrupt: []func(){ + func() { + body.SetSessionKey(nil) + }, + func() { + body.SetSessionKey([]byte{}) + }, + }, + restore: restoreAuthKey, + assert: func(val session.Object) { + require.True(t, val.AssertAuthKey(&authKey)) + }, + breakSign: func(m *v2session.Token) { + body := m.GetBody() + key := body.GetSessionKey() + cp := slice.Copy(key) + cp[len(cp)-1]++ + body.SetSessionKey(cp) + }, + }, + { + name: "Context", + corrupt: []func(){ + func() { + body.SetContext(nil) + }, + func() { + cObj.SetAddress(new(refs.Address)) + }, + func() { + var brokenCnr refs.ContainerID + brokenCnr.SetValue(append(addrV2.GetContainerID().GetValue(), 1)) + var addr refs.Address + addr.SetContainerID(&brokenCnr) + cObj.SetAddress(&addr) + }, + func() { + var brokenObj refs.ObjectID + brokenObj.SetValue(append(addrV2.GetContainerID().GetValue(), 1)) + var addr refs.Address + addr.SetObjectID(&brokenObj) + cObj.SetAddress(&addr) + }, + }, + restore: restoreCtx, + assert: func(val session.Object) { + require.True(t, val.AssertContainer(cnr)) + require.False(t, val.AssertContainer(cidtest.ID())) + require.True(t, val.AssertObject(obj)) + require.False(t, val.AssertObject(oidtest.ID())) + }, + breakSign: func(m *v2session.Token) { + cnr := m.GetBody().GetContext().(*v2session.ObjectSessionContext).GetAddress().GetContainerID().GetValue() + cnr[len(cnr)-1]++ + }, + }, + } { + var val session.Object + + for i, corrupt := range testcase.corrupt { + corrupt() + require.Error(t, val.ReadFromV2(validV2), testcase.name, fmt.Sprintf("corrupt #%d", i)) + + testcase.restore() + require.NoError(t, val.ReadFromV2(validV2), testcase.name, fmt.Sprintf("corrupt #%d", i)) + + if testcase.assert != nil { + testcase.assert(val) + } + + if testcase.breakSign != nil { + require.NoError(t, val.Sign(signer), testcase.name) + require.True(t, val.VerifySignature(), testcase.name) + + var signedV2 v2session.Token + val.WriteToV2(&signedV2) + + var restored session.Object + require.NoError(t, restored.ReadFromV2(signedV2), testcase.name) + require.True(t, restored.VerifySignature(), testcase.name) + + testcase.breakSign(&signedV2) + + require.NoError(t, restored.ReadFromV2(signedV2), testcase.name) + require.False(t, restored.VerifySignature(), testcase.name) + } + } + } +} + +func TestObject_WriteToV2(t *testing.T) { + var val session.Object + + assert := func(baseAssert func(v2session.Token)) { + var m v2session.Token + val.WriteToV2(&m) + baseAssert(m) + } + + // ID + id := uuid.New() + + binID, err := id.MarshalBinary() + require.NoError(t, err) + + val.SetID(id) + assert(func(m v2session.Token) { + require.Equal(t, binID, m.GetBody().GetID()) + }) + + // Owner/Signature + signer := randSigner() + + require.NoError(t, val.Sign(signer)) + + var usr user.ID + user.IDFromKey(&usr, signer.PublicKey) + + var usrV2 refs.OwnerID + usr.WriteToV2(&usrV2) + + assert(func(m v2session.Token) { + require.Equal(t, &usrV2, m.GetBody().GetOwnerID()) + + sig := m.GetSignature() + require.NotZero(t, sig.GetKey()) + require.NotZero(t, sig.GetSign()) + }) + + // Lifetime + const iat, nbf, exp = 1, 2, 3 + val.SetIat(iat) + val.SetNbf(nbf) + val.SetExp(exp) + + assert(func(m v2session.Token) { + lt := m.GetBody().GetLifetime() + require.EqualValues(t, iat, lt.GetIat()) + require.EqualValues(t, nbf, lt.GetNbf()) + require.EqualValues(t, exp, lt.GetExp()) + }) + + // Context + assert(func(m v2session.Token) { + cCnr, ok := m.GetBody().GetContext().(*v2session.ObjectSessionContext) + require.True(t, ok) + require.Zero(t, cCnr.GetAddress()) + }) cnr := cidtest.ID() var cnrV2 refs.ContainerID cnr.WriteToV2(&cnrV2) - var addrV2 refs.Address - addrV2.SetContainerID(&cnrV2) + obj := oidtest.ID() - t.Run("protocol violation", func(t *testing.T) { - require.Error(t, x.ReadFromV2(m)) + var objV2 refs.ObjectID + obj.WriteToV2(&objV2) - m.SetBody(&b) + val.BindContainer(cnr) + val.LimitByObject(obj) - require.Error(t, x.ReadFromV2(m)) - - b.SetID(id[:]) - - require.Error(t, x.ReadFromV2(m)) - - b.SetContext(&c) - - require.Error(t, x.ReadFromV2(m)) - - c.SetAddress(&addrV2) - - require.NoError(t, x.ReadFromV2(m)) - }) - - m.SetBody(&b) - c.SetAddress(&addrV2) - b.SetContext(&c) - b.SetID(id[:]) - - t.Run("object", func(t *testing.T) { - require.NoError(t, x.ReadFromV2(m)) - require.True(t, x.AssertContainer(cnr)) - - obj := oidtest.Address() - - var objV2 refs.Address - obj.WriteToV2(&objV2) - - c.SetAddress(&objV2) - - require.NoError(t, x.ReadFromV2(m)) - require.True(t, x.AssertContainer(obj.Container())) - require.True(t, x.AssertObject(obj.Object())) - }) - - t.Run("verb", func(t *testing.T) { - require.NoError(t, x.ReadFromV2(m)) - require.True(t, x.AssertVerb(0)) - - verb := v2session.ObjectSessionVerb(rand.Uint32()) - - c.SetVerb(verb) - - require.NoError(t, x.ReadFromV2(m)) - require.True(t, x.AssertVerb(session.ObjectVerb(verb))) - }) - - t.Run("id", func(t *testing.T) { - id := uuid.New() - bID := id[:] - - b.SetID(bID) - - require.NoError(t, x.ReadFromV2(m)) - require.Equal(t, id, x.ID()) - }) - - t.Run("lifetime", func(t *testing.T) { - const nbf, iat, exp = 11, 22, 33 - - var lt v2session.TokenLifetime - lt.SetNbf(nbf) - lt.SetIat(iat) - lt.SetExp(exp) - - b.SetLifetime(<) - - require.NoError(t, x.ReadFromV2(m)) - require.False(t, x.ExpiredAt(exp-1)) - require.True(t, x.ExpiredAt(exp)) - require.True(t, x.ExpiredAt(exp+1)) - require.True(t, x.InvalidAt(nbf-1)) - require.True(t, x.InvalidAt(iat-1)) - require.False(t, x.InvalidAt(iat)) - require.False(t, x.InvalidAt(exp-1)) - require.True(t, x.InvalidAt(exp)) - require.True(t, x.InvalidAt(exp+1)) - }) - - t.Run("session key", func(t *testing.T) { - key := randPublicKey() - - bKey := make([]byte, key.MaxEncodedSize()) - bKey = bKey[:key.Encode(bKey)] - - b.SetSessionKey(bKey) - - require.NoError(t, x.ReadFromV2(m)) - require.True(t, x.AssertAuthKey(key)) - }) -} - -func TestEncodingObject(t *testing.T) { - tok := *sessiontest.ObjectSigned() - - t.Run("binary", func(t *testing.T) { - data := tok.Marshal() - - var tok2 session.Object - require.NoError(t, tok2.Unmarshal(data)) - - require.Equal(t, tok, tok2) - }) - - t.Run("json", func(t *testing.T) { - data, err := tok.MarshalJSON() - require.NoError(t, err) - - var tok2 session.Object - require.NoError(t, tok2.UnmarshalJSON(data)) - - require.Equal(t, tok, tok2) + assert(func(m v2session.Token) { + cCnr, ok := m.GetBody().GetContext().(*v2session.ObjectSessionContext) + require.True(t, ok) + require.Equal(t, &cnrV2, cCnr.GetAddress().GetContainerID()) + require.Equal(t, &objV2, cCnr.GetAddress().GetObjectID()) }) } func TestObject_BindContainer(t *testing.T) { + var val session.Object + var m v2session.Token + filled := sessiontest.Object() + + assertDefaults := func() { + cCnr, ok := m.GetBody().GetContext().(*v2session.ObjectSessionContext) + require.True(t, ok) + require.Zero(t, cCnr.GetAddress()) + } + + assertBinary := func(baseAssert func()) { + val2 := filled + + require.NoError(t, val2.Unmarshal(val.Marshal())) + baseAssert() + } + + assertJSON := func(baseAssert func()) { + val2 := filled + + jd, err := val.MarshalJSON() + require.NoError(t, err) + + require.NoError(t, val2.UnmarshalJSON(jd)) + baseAssert() + } + + val.WriteToV2(&m) + + assertDefaults() + assertBinary(assertDefaults) + assertJSON(assertDefaults) + + // set value + + cnr := cidtest.ID() + + var cnrV2 refs.ContainerID + cnr.WriteToV2(&cnrV2) + + val.BindContainer(cnr) + + val.WriteToV2(&m) + + assertCnr := func() { + cObj, ok := m.GetBody().GetContext().(*v2session.ObjectSessionContext) + require.True(t, ok) + require.Equal(t, &cnrV2, cObj.GetAddress().GetContainerID()) + } + + assertCnr() + assertBinary(assertCnr) + assertJSON(assertCnr) +} + +func TestObject_AssertContainer(t *testing.T) { var x session.Object cnr := cidtest.ID() @@ -184,6 +422,62 @@ func TestObject_BindContainer(t *testing.T) { } func TestObject_LimitByObject(t *testing.T) { + var val session.Object + var m v2session.Token + filled := sessiontest.Object() + + assertDefaults := func() { + cCnr, ok := m.GetBody().GetContext().(*v2session.ObjectSessionContext) + require.True(t, ok) + require.Zero(t, cCnr.GetAddress()) + } + + assertBinary := func(baseAssert func()) { + val2 := filled + + require.NoError(t, val2.Unmarshal(val.Marshal())) + baseAssert() + } + + assertJSON := func(baseAssert func()) { + val2 := filled + + jd, err := val.MarshalJSON() + require.NoError(t, err) + + require.NoError(t, val2.UnmarshalJSON(jd)) + baseAssert() + } + + val.WriteToV2(&m) + + assertDefaults() + assertBinary(assertDefaults) + assertJSON(assertDefaults) + + // set value + + obj := oidtest.ID() + + var objV2 refs.ObjectID + obj.WriteToV2(&objV2) + + val.LimitByObject(obj) + + val.WriteToV2(&m) + + assertObj := func() { + cObj, ok := m.GetBody().GetContext().(*v2session.ObjectSessionContext) + require.True(t, ok) + require.Equal(t, &objV2, cObj.GetAddress().GetObjectID()) + } + + assertObj() + assertBinary(assertObj) + assertJSON(assertObj) +} + +func TestObject_AssertObject(t *testing.T) { var x session.Object obj := oidtest.ID() @@ -198,21 +492,7 @@ func TestObject_LimitByObject(t *testing.T) { require.False(t, x.AssertObject(obj2)) } -func TestObjectExp(t *testing.T) { - var x session.Object - - exp := rand.Uint64() - - require.True(t, x.ExpiredAt(exp)) - - x.SetExp(exp) - - require.False(t, x.ExpiredAt(exp-1)) - require.True(t, x.ExpiredAt(exp)) - require.True(t, x.ExpiredAt(exp+1)) -} - -func TestObjectLifetime(t *testing.T) { +func TestObject_InvalidAt(t *testing.T) { var x session.Object nbf := rand.Uint64() @@ -233,7 +513,7 @@ func TestObjectLifetime(t *testing.T) { require.True(t, x.InvalidAt(exp)) } -func TestObjectID(t *testing.T) { +func TestObject_ID(t *testing.T) { var x session.Object require.Zero(t, x.ID()) @@ -245,7 +525,7 @@ func TestObjectID(t *testing.T) { require.Equal(t, id, x.ID()) } -func TestObjectAuthKey(t *testing.T) { +func TestObject_AssertAuthKey(t *testing.T) { var x session.Object key := randPublicKey() @@ -257,7 +537,68 @@ func TestObjectAuthKey(t *testing.T) { require.True(t, x.AssertAuthKey(key)) } -func TestObjectVerb(t *testing.T) { +func TestObject_ForVerb(t *testing.T) { + var val session.Object + var m v2session.Token + filled := sessiontest.Object() + + assertDefaults := func() { + cCnr, ok := m.GetBody().GetContext().(*v2session.ObjectSessionContext) + require.True(t, ok) + require.Zero(t, cCnr.GetVerb()) + } + + assertBinary := func(baseAssert func()) { + val2 := filled + + require.NoError(t, val2.Unmarshal(val.Marshal())) + baseAssert() + } + + assertJSON := func(baseAssert func()) { + val2 := filled + + jd, err := val.MarshalJSON() + require.NoError(t, err) + + require.NoError(t, val2.UnmarshalJSON(jd)) + baseAssert() + } + + val.WriteToV2(&m) + + assertDefaults() + assertBinary(assertDefaults) + assertJSON(assertDefaults) + + // set value + + assertVerb := func(verb v2session.ObjectSessionVerb) { + cCnr, ok := m.GetBody().GetContext().(*v2session.ObjectSessionContext) + require.True(t, ok) + require.Equal(t, verb, cCnr.GetVerb()) + } + + for from, to := range map[session.ObjectVerb]v2session.ObjectSessionVerb{ + session.VerbObjectPut: v2session.ObjectVerbPut, + session.VerbObjectGet: v2session.ObjectVerbGet, + session.VerbObjectHead: v2session.ObjectVerbHead, + session.VerbObjectSearch: v2session.ObjectVerbSearch, + session.VerbObjectRangeHash: v2session.ObjectVerbRangeHash, + session.VerbObjectRange: v2session.ObjectVerbRange, + session.VerbObjectDelete: v2session.ObjectVerbDelete, + } { + val.ForVerb(from) + + val.WriteToV2(&m) + + assertVerb(to) + assertBinary(func() { assertVerb(to) }) + assertJSON(func() { assertVerb(to) }) + } +} + +func TestObject_AssertVerb(t *testing.T) { var x session.Object const v1, v2 = session.VerbObjectGet, session.VerbObjectPut @@ -271,64 +612,6 @@ func TestObjectVerb(t *testing.T) { require.True(t, x.AssertVerb(v2, v1)) } -func TestObjectSignature(t *testing.T) { - var x session.Object - - const nbf = 11 - const iat = 22 - const exp = 33 - id := uuid.New() - key := randPublicKey() - cnr := cidtest.ID() - obj := oidtest.ID() - verb := session.VerbObjectDelete - - signer := randSigner() - - fs := []func(){ - func() { x.SetNbf(nbf) }, - func() { x.SetNbf(nbf + 1) }, - - func() { x.SetIat(iat) }, - func() { x.SetIat(iat + 1) }, - - func() { x.SetExp(exp) }, - func() { x.SetExp(exp + 1) }, - - func() { x.SetID(id) }, - func() { - idcp := id - idcp[0]++ - x.SetID(idcp) - }, - - func() { x.SetAuthKey(key) }, - func() { x.SetAuthKey(randPublicKey()) }, - - func() { x.BindContainer(cnr) }, - func() { x.BindContainer(cidtest.ID()) }, - - func() { x.LimitByObject(obj) }, - func() { x.LimitByObject(oidtest.ID()) }, - - func() { x.ForVerb(verb) }, - func() { x.ForVerb(verb + 1) }, - } - - for i := 0; i < len(fs); i += 2 { - fs[i]() - - require.NoError(t, x.Sign(signer)) - require.True(t, x.VerifySignature()) - - fs[i+1]() - require.False(t, x.VerifySignature()) - - fs[i]() - require.True(t, x.VerifySignature()) - } -} - func TestObject_Issuer(t *testing.T) { var token session.Object signer := randSigner()