diff --git a/client/container_statistic_test.go b/client/container_statistic_test.go index 8c8c51bf..a3dda510 100644 --- a/client/container_statistic_test.go +++ b/client/container_statistic_test.go @@ -24,6 +24,7 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/container" "github.com/nspcc-dev/neofs-sdk-go/container/acl" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" "github.com/nspcc-dev/neofs-sdk-go/crypto/test" "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/nspcc-dev/neofs-sdk-go/netmap" @@ -479,10 +480,7 @@ func TestClientStatistic_ContainerEndpointInfo(t *testing.T) { var ver refs.Version var nodeInfo netmapv2.NodeInfo - b := make([]byte, signer.Public().MaxEncodedSize()) - signer.Public().Encode(b) - - nodeInfo.SetPublicKey(b) + nodeInfo.SetPublicKey(neofscrypto.PublicKeyBytes(signer.Public())) nodeInfo.SetAddresses("https://some-endpont.com") body := netmapv2.LocalNodeInfoResponseBody{} @@ -553,9 +551,7 @@ func TestClientStatistic_CreateSession(t *testing.T) { body := session.CreateResponseBody{} body.SetID(randBytes(10)) - b := make([]byte, signer.Public().MaxEncodedSize()) - signer.Public().Encode(b) - body.SetSessionKey(b) + body.SetSessionKey(neofscrypto.PublicKeyBytes(signer.Public())) resp.SetBody(&body) resp.SetMetaHeader(&meta) diff --git a/client/example_container_put_test.go b/client/example_container_put_test.go index 08c2e454..3f8f6a39 100644 --- a/client/example_container_put_test.go +++ b/client/example_container_put_test.go @@ -25,7 +25,7 @@ func ExampleClient_ContainerPut() { panic(err) } - signer := user.NewSignerRFC6979(key.PrivateKey) + signer := user.NewAutoIDSignerRFC6979(key.PrivateKey) // take account from user's signer accountID = signer.UserID() diff --git a/crypto/signature.go b/crypto/signature.go index a0229c63..0467de4d 100644 --- a/crypto/signature.go +++ b/crypto/signature.go @@ -28,19 +28,19 @@ type Signature refs.Signature // // See also WriteToV2. func (x *Signature) ReadFromV2(m refs.Signature) error { - if len(m.GetKey()) == 0 { + bPubKey := m.GetKey() + if len(bPubKey) == 0 { return errors.New("missing public key") - } else if len(m.GetSign()) == 0 { + } + + sig := m.GetSign() + if len(sig) == 0 { return errors.New("missing signature") } - switch m.GetScheme() { - default: - return fmt.Errorf("unsupported scheme %v", m.GetSign()) - case - refs.ECDSA_SHA512, - refs.ECDSA_RFC6979_SHA256, - refs.ECDSA_RFC6979_SHA256_WALLET_CONNECT: + _, err := decodePublicKey(m) + if err != nil { + return err } *x = Signature(m) @@ -53,7 +53,7 @@ func (x *Signature) ReadFromV2(m refs.Signature) error { // // See also ReadFromV2. func (x Signature) WriteToV2(m *refs.Signature) { - *m = (refs.Signature)(x) + *m = refs.Signature(x) } // Calculate signs data using Signer and encodes public key for subsequent @@ -100,21 +100,11 @@ func (x *Signature) CalculateMarshalled(signer Signer, obj StablyMarshallable) e // // See also Calculate. func (x Signature) Verify(data []byte) bool { - m := (*refs.Signature)(&x) + m := refs.Signature(x) - f, ok := publicKeys[Scheme(m.GetScheme())] - if !ok { - return false - } - - key := f() - - err := key.Decode(m.GetKey()) - if err != nil { - return false - } + key, err := decodePublicKey(m) - return key.Verify(data, m.GetSign()) + return err == nil && key.Verify(data, m.GetSign()) } func (x *Signature) fillSignature(signer Signer, signature []byte) { @@ -126,5 +116,61 @@ func (x *Signature) fillSignature(signer Signer, signature []byte) { m := (*refs.Signature)(x) m.SetScheme(refs.SignatureScheme(signer.Scheme())) m.SetSign(signature) - m.SetKey(key) + m.SetKey(PublicKeyBytes(signer.Public())) +} + +// Scheme returns signature scheme used by signer to calculate the signature. +// +// Scheme MUST NOT be called before [Signature.ReadFromV2] or +// [Signature.Calculate] methods. +func (x Signature) Scheme() Scheme { + return Scheme((*refs.Signature)(&x).GetScheme()) +} + +// PublicKey returns public key of the signer which calculated the signature. +// +// PublicKey MUST NOT be called before [Signature.ReadFromV2] or +// [Signature.Calculate] methods. +// +// See also [Signature.PublicKeyBytes]. +func (x Signature) PublicKey() PublicKey { + key, _ := decodePublicKey(refs.Signature(x)) + return key +} + +// PublicKeyBytes returns binary-encoded public key of the signer which +// calculated the signature. +// +// PublicKeyBytes MUST NOT be called before [Signature.ReadFromV2] or +// [Signature.Calculate] methods. +// +// See also [Signature.PublicKey]. +func (x Signature) PublicKeyBytes() []byte { + return (*refs.Signature)(&x).GetKey() +} + +// Value returns calculated digital signature. +// +// Value MUST NOT be called before [Signature.ReadFromV2] or +// [Signature.Calculate] methods. +func (x Signature) Value() []byte { + return (*refs.Signature)(&x).GetSign() +} + +func decodePublicKey(m refs.Signature) (PublicKey, error) { + scheme := Scheme(m.GetScheme()) + + newPubKey, ok := publicKeys[scheme] + if !ok { + return nil, fmt.Errorf("unsupported scheme %d", scheme) + } + + pubKey := newPubKey() + + err := pubKey.Decode(m.GetKey()) + if err != nil { + return nil, fmt.Errorf("decode public key from binary: %w", err) + } + + return pubKey, nil } diff --git a/crypto/signature_test.go b/crypto/signature_test.go new file mode 100644 index 00000000..87953aab --- /dev/null +++ b/crypto/signature_test.go @@ -0,0 +1,68 @@ +package neofscrypto_test + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/refs" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + "github.com/nspcc-dev/neofs-sdk-go/crypto/test" + "github.com/stretchr/testify/require" +) + +const anyUnsupportedScheme = neofscrypto.ECDSA_WALLETCONNECT + 1 + +func TestSignatureLifecycle(t *testing.T) { + data := []byte("Hello, world!") + signer := test.RandomSigner(t) + scheme := signer.Scheme() + pubKey := signer.Public() + bPubKey := neofscrypto.PublicKeyBytes(pubKey) + + var clientSig neofscrypto.Signature + + err := clientSig.Calculate(signer, data) + require.NoError(t, err) + + testSig := func(sig neofscrypto.Signature) { + require.Equal(t, signer.Scheme(), sig.Scheme()) + require.Equal(t, signer.Public(), sig.PublicKey()) + require.Equal(t, bPubKey, sig.PublicKeyBytes()) + require.NotEmpty(t, sig.Value()) + require.True(t, sig.Verify(data)) + } + + testSig(clientSig) + + var sigV2 refs.Signature + clientSig.WriteToV2(&sigV2) + + require.Equal(t, refs.SignatureScheme(scheme), sigV2.GetScheme()) + require.Equal(t, bPubKey, sigV2.GetKey()) + require.Equal(t, clientSig.Value(), sigV2.GetSign()) + + // sigV2 transmitted to server over the network + + var serverSig neofscrypto.Signature + + err = serverSig.ReadFromV2(sigV2) + require.NoError(t, err) + + testSig(serverSig) + + // break the message in different ways + for i, breakSig := range []func(*refs.Signature){ + func(sigV2 *refs.Signature) { sigV2.SetScheme(refs.SignatureScheme(anyUnsupportedScheme)) }, + func(sigV2 *refs.Signature) { + key := sigV2.GetKey() + sigV2.SetKey(key[:len(key)-1]) + }, + func(sigV2 *refs.Signature) { sigV2.SetKey(append(sigV2.GetKey(), 1)) }, + func(sigV2 *refs.Signature) { sigV2.SetSign(nil) }, + } { + sigV2Cp := sigV2 + breakSig(&sigV2Cp) + + err = serverSig.ReadFromV2(sigV2Cp) + require.Errorf(t, err, "break func #%d", i) + } +} diff --git a/crypto/signer.go b/crypto/signer.go index 530cbe84..a1034b92 100644 --- a/crypto/signer.go +++ b/crypto/signer.go @@ -89,6 +89,8 @@ type PublicKey interface { // on any failure except (*). // // Encode is a reverse operation to Decode. + // + // [PublicKeyBytes] may be used to skip explicit buffer allocation. Encode(buf []byte) int // Decode decodes binary public key. diff --git a/crypto/test/tests.go b/crypto/test/tests.go index cb321ddd..f9135073 100644 --- a/crypto/test/tests.go +++ b/crypto/test/tests.go @@ -30,5 +30,5 @@ func RandomSignerRFC6979(tb testing.TB) user.Signer { p, err := keys.NewPrivateKey() require.NoError(tb, err) - return user.NewSignerRFC6979(p.PrivateKey) + return user.NewAutoIDSignerRFC6979(p.PrivateKey) } diff --git a/crypto/util.go b/crypto/util.go index e6f87a31..f0a3bd62 100644 --- a/crypto/util.go +++ b/crypto/util.go @@ -7,3 +7,10 @@ import "encoding/hex" func StringifyKeyBinary(src []byte) string { return hex.EncodeToString(src) } + +// PublicKeyBytes returns binary-encoded PublicKey. Use [PublicKey.Encode] to +// avoid new slice allocation. +func PublicKeyBytes(pubKey PublicKey) []byte { + b := make([]byte, pubKey.MaxEncodedSize()) + return b[:pubKey.Encode(b)] +} diff --git a/crypto/util_test.go b/crypto/util_test.go new file mode 100644 index 00000000..feb7a8e1 --- /dev/null +++ b/crypto/util_test.go @@ -0,0 +1,26 @@ +package neofscrypto_test + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" + "github.com/stretchr/testify/require" +) + +func TestPublicKeyBytes(t *testing.T) { + k, err := keys.NewPrivateKey() + require.NoError(t, err) + + pubKey := neofsecdsa.PublicKey(k.PrivateKey.PublicKey) + + bPubKey := neofscrypto.PublicKeyBytes(&pubKey) + + var restoredPubKey neofsecdsa.PublicKey + + err = restoredPubKey.Decode(bPubKey) + require.NoError(t, err) + + require.Equal(t, pubKey, restoredPubKey) +} diff --git a/object/slicer/slicer_test.go b/object/slicer/slicer_test.go index 6d2e949e..b4e520cb 100644 --- a/object/slicer/slicer_test.go +++ b/object/slicer/slicer_test.go @@ -220,7 +220,7 @@ func randomInput(tb testing.TB, size, sizeLimit uint64) (input, slicer.Options) } var in input - in.signer = user.NewSignerRFC6979(*key) + in.signer = user.NewAutoIDSigner(*key) in.container = cidtest.ID() in.currentEpoch = rand.Uint64() if sizeLimit > 0 { diff --git a/pool/example_pool_test.go b/pool/example_pool_test.go index d47bc32c..22246ca8 100644 --- a/pool/example_pool_test.go +++ b/pool/example_pool_test.go @@ -13,7 +13,7 @@ import ( func ExampleNew_easiestWay() { // Signer generation, like example. pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - signer := user.NewSignerRFC6979(*pk) + signer := user.NewAutoIDSignerRFC6979(*pk) pool, _ := New(NewFlatNodeParams([]string{"grpc://localhost:8080", "grpcs://localhost:8081"}), signer, DefaultOptions()) _ = pool @@ -24,7 +24,7 @@ func ExampleNew_easiestWay() { func ExampleNew_adjustingParameters() { // Signer generation, like example. pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - signer := user.NewSignerRFC6979(*pk) + signer := user.NewAutoIDSignerRFC6979(*pk) opts := DefaultOptions() opts.SetErrorThreshold(10) diff --git a/pool/pool.go b/pool/pool.go index a7084674..f03d9d2b 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -1663,19 +1663,13 @@ func (p *innerPool) connection() (internalClient, error) { } func formCacheKey(address string, signer neofscrypto.Signer) string { - b := make([]byte, signer.Public().MaxEncodedSize()) - signer.Public().Encode(b) - - return address + string(b) + return address + string(neofscrypto.PublicKeyBytes(signer.Public())) } // cacheKeyForSession generates cache key for a signed session token. // It is used with pool methods compatible with [sdkClient.Client]. func cacheKeyForSession(address string, signer neofscrypto.Signer, verb session.ObjectVerb, cnr cid.ID) string { - b := make([]byte, signer.Public().MaxEncodedSize()) - signer.Public().Encode(b) - - return fmt.Sprintf("%s%s%d%s", address, b, verb, cnr) + return fmt.Sprintf("%s%s%d%s", address, neofscrypto.PublicKeyBytes(signer.Public()), verb, cnr) } func (p *Pool) checkSessionTokenErr(err error, address string, cl internalClient) bool { diff --git a/session/common.go b/session/common.go index f5271253..244d81bd 100644 --- a/session/common.go +++ b/session/common.go @@ -303,8 +303,7 @@ func (x commonData) ID() uuid.UUID { // // See also AssertAuthKey. func (x *commonData) SetAuthKey(key neofscrypto.PublicKey) { - x.authKey = make([]byte, key.MaxEncodedSize()) - x.authKey = x.authKey[:key.Encode(x.authKey)] + x.authKey = neofscrypto.PublicKeyBytes(key) } // SetIssuer allows to set issuer before Sign call. @@ -321,10 +320,7 @@ func (x *commonData) SetIssuer(id user.ID) { // // 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) + return bytes.Equal(neofscrypto.PublicKeyBytes(key), x.authKey) } // Issuer returns user ID of the session issuer. @@ -340,3 +336,14 @@ func (x commonData) Issuer() user.ID { return user.ID{} } + +// IssuerPublicKeyBytes returns binary-encoded public key of the session issuer. +// +// IssuerPublicKeyBytes MUST NOT be called before ReadFromV2 or Sign methods. +func (x *commonData) IssuerPublicKeyBytes() []byte { + if x.sigSet { + return x.sig.GetKey() + } + + return nil +} diff --git a/session/container_test.go b/session/container_test.go index f24bfbf7..444cdc5d 100644 --- a/session/container_test.go +++ b/session/container_test.go @@ -57,8 +57,7 @@ func TestContainerProtocolV2(t *testing.T) { // Session key signer := test.RandomSignerRFC6979(t) authKey := signer.Public() - binAuthKey := make([]byte, authKey.MaxEncodedSize()) - binAuthKey = binAuthKey[:authKey.Encode(binAuthKey)] + binAuthKey := neofscrypto.PublicKeyBytes(authKey) restoreAuthKey := func() { body.SetSessionKey(binAuthKey) } @@ -524,9 +523,9 @@ func TestIssuedBy(t *testing.T) { } func TestContainer_Issuer(t *testing.T) { - var token session.Container - t.Run("signer", func(t *testing.T) { + var token session.Container + signer := test.RandomSignerRFC6979(t) require.Zero(t, token.Issuer()) @@ -537,12 +536,25 @@ func TestContainer_Issuer(t *testing.T) { }) t.Run("external", func(t *testing.T) { + var token session.Container + signer := test.RandomSignerRFC6979(t) issuer := signer.UserID() token.SetIssuer(issuer) require.True(t, token.Issuer().Equals(issuer)) }) + + t.Run("public key", func(t *testing.T) { + var token session.Container + + signer := test.RandomSignerRFC6979(t) + + require.Nil(t, token.IssuerPublicKeyBytes()) + require.NoError(t, token.Sign(signer)) + + require.Equal(t, neofscrypto.PublicKeyBytes(signer.Public()), token.IssuerPublicKeyBytes()) + }) } func TestContainer_Sign(t *testing.T) { diff --git a/session/object_test.go b/session/object_test.go index f0fdca7c..21524c4b 100644 --- a/session/object_test.go +++ b/session/object_test.go @@ -11,6 +11,7 @@ import ( "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" "github.com/nspcc-dev/neofs-sdk-go/crypto/test" oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" "github.com/nspcc-dev/neofs-sdk-go/session" @@ -56,8 +57,7 @@ func TestObjectProtocolV2(t *testing.T) { // Session key signer := test.RandomSignerRFC6979(t) authKey := signer.Public() - binAuthKey := make([]byte, authKey.MaxEncodedSize()) - binAuthKey = binAuthKey[:authKey.Encode(binAuthKey)] + binAuthKey := neofscrypto.PublicKeyBytes(authKey) restoreAuthKey := func() { body.SetSessionKey(binAuthKey) } @@ -610,12 +610,14 @@ func TestObject_Issuer(t *testing.T) { signer := test.RandomSignerRFC6979(t) require.Zero(t, token.Issuer()) + require.Nil(t, token.IssuerPublicKeyBytes()) require.NoError(t, token.Sign(signer)) issuer := signer.UserID() require.True(t, token.Issuer().Equals(issuer)) + require.Equal(t, neofscrypto.PublicKeyBytes(signer.Public()), token.IssuerPublicKeyBytes()) } func TestObject_Sign(t *testing.T) { diff --git a/user/signer.go b/user/signer.go index 582a39d7..27b934bd 100644 --- a/user/signer.go +++ b/user/signer.go @@ -8,65 +8,53 @@ import ( neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" ) -// Signer is an interface of entities that can be used for signing operations -// in NeoFS. It is the same as [neofscrypto.Signer], but has an extra method to retrieve [ID]. +// Signer represents a NeoFS user authorized by a digital signature. type Signer interface { + // Signer signs data on behalf of the user. neofscrypto.Signer - + // UserID returns ID of the associated user. UserID() ID } -// SignerRFC6979 wraps [ecdsa.PrivateKey] and represents signer based on deterministic -// ECDSA with SHA-256 hashing (RFC 6979). Provides [Signer] interface. -// -// Instances SHOULD be initialized with [NewSignerRFC6979] or [NewSignerRFC6979WithID]. -type SignerRFC6979 struct { - neofsecdsa.SignerRFC6979 - userID ID +type signer struct { + neofscrypto.Signer + usr ID } -// NewSignerRFC6979 is a constructor for [SignerRFC6979]. -func NewSignerRFC6979(pk ecdsa.PrivateKey) *SignerRFC6979 { - var id ID - id.SetScriptHash((*keys.PublicKey)(&pk.PublicKey).GetScriptHash()) - - return &SignerRFC6979{ - userID: id, - SignerRFC6979: neofsecdsa.SignerRFC6979(pk), - } +func (s signer) UserID() ID { + return s.usr } -// NewSignerRFC6979WithID is a constructor for [SignerRFC6979] where you may specify [ID] associated with this signer. -func NewSignerRFC6979WithID(pk ecdsa.PrivateKey, id ID) *SignerRFC6979 { - return &SignerRFC6979{ - SignerRFC6979: neofsecdsa.SignerRFC6979(pk), - userID: id, +// NewSigner combines provided [neofscrypto.Signer] and [ID] into [Signer]. +// +// See also [NewAutoIDSigner]. +func NewSigner(s neofscrypto.Signer, usr ID) Signer { + return signer{ + Signer: s, + usr: usr, } } -// UserID returns the [ID] using script hash calculated for the given key. -func (s SignerRFC6979) UserID() ID { - return s.userID -} +func newAutoResolvedSigner(s neofscrypto.Signer, pubKey ecdsa.PublicKey) Signer { + var id ID + id.SetScriptHash((*keys.PublicKey)(&pubKey).GetScriptHash()) -// StaticSigner emulates real sign and contains already precalculated hash. -// Provides [Signer] interface. -type StaticSigner struct { - neofscrypto.StaticSigner - id ID + return NewSigner(s, id) } -// NewStaticSignerWithID creates new StaticSigner with specified [ID]. -func NewStaticSignerWithID(scheme neofscrypto.Scheme, sig []byte, pubKey neofscrypto.PublicKey, id ID) *StaticSigner { - return &StaticSigner{ - StaticSigner: *neofscrypto.NewStaticSigner(scheme, sig, pubKey), - id: id, - } +// NewAutoIDSigner returns [Signer] with neofscrypto.ECDSA_SHA512 +// signature scheme and user [ID] automatically resolved from the ECDSA public +// key. +// +// See also [NewAutoIDSignerRFC6979]. +func NewAutoIDSigner(key ecdsa.PrivateKey) Signer { + return newAutoResolvedSigner(neofsecdsa.Signer(key), key.PublicKey) } -// UserID returns underlying [ID]. -func (s *StaticSigner) UserID() ID { - return s.id +// NewAutoIDSignerRFC6979 is an analogue of [NewAutoIDSigner] but with +// [neofscrypto.ECDSA_DETERMINISTIC_SHA256] signature scheme. +func NewAutoIDSignerRFC6979(key ecdsa.PrivateKey) Signer { + return newAutoResolvedSigner(neofsecdsa.SignerRFC6979(key), key.PublicKey) } // ResolveFromECDSAPublicKey resolves [ID] from the given [ecdsa.PublicKey]. diff --git a/waiter/example_waiter_test.go b/waiter/example_waiter_test.go index ec865c6e..a6e58ae0 100644 --- a/waiter/example_waiter_test.go +++ b/waiter/example_waiter_test.go @@ -24,7 +24,7 @@ func ExampleNewWaiter() { panic(err) } - signer := user.NewSignerRFC6979(key.PrivateKey) + signer := user.NewAutoIDSignerRFC6979(key.PrivateKey) account := signer.UserID()