diff --git a/service/errors.go b/service/errors.go index 4aefb4e..1154a28 100644 --- a/service/errors.go +++ b/service/errors.go @@ -16,3 +16,6 @@ const ErrCannotFindOwner = internal.Error("cannot find owner public key") // ErrWrongOwner is raised when passed OwnerID not equal to present PublicKey const ErrWrongOwner = internal.Error("wrong owner") + +// ErrNilSignedDataSource returned by functions that expect a non-nil SignedDataSource argument, but received nil. +const ErrNilSignedDataSource = internal.Error("signed data source is nil") diff --git a/service/sign.go b/service/sign.go new file mode 100644 index 0000000..f371483 --- /dev/null +++ b/service/sign.go @@ -0,0 +1,58 @@ +package service + +import ( + "crypto/ecdsa" + + crypto "github.com/nspcc-dev/neofs-crypto" +) + +// Returns data from DataSignatureAccumulator for signature creation/verification. +// +// If passed DataSignatureAccumulator provides a SignedDataReader interface, data for signature is obtained +// using this interface for optimization. In this case, it is understood that reading into the slice D +// that the method DataForSignature returns does not change D. +func dataForSignature(src SignedDataSource) ([]byte, error) { + r, ok := src.(SignedDataReader) + if !ok { + return src.SignedData() + } + + buf := bytesPool.Get().([]byte) + defer func() { + bytesPool.Put(buf) + }() + + if size := r.SignedDataSize(); size <= cap(buf) { + buf = buf[:size] + } else { + buf = make([]byte, size) + } + + n, err := r.ReadSignedData(buf) + if err != nil { + return nil, err + } + + return buf[:n], nil + +} + +// DataSignature returns the signature of data obtained using the private key. +// +// If passed data container is nil, ErrNilSignedDataSource returns. +// If passed private key is nil, crypto.ErrEmptyPrivateKey returns. +// If the data container or the signature function returns an error, it is returned directly. +func DataSignature(src SignedDataSource, key *ecdsa.PrivateKey) ([]byte, error) { + if src == nil { + return nil, ErrNilSignedDataSource + } else if key == nil { + return nil, crypto.ErrEmptyPrivateKey + } + + data, err := dataForSignature(src) + if err != nil { + return nil, err + } + + return crypto.Sign(key, data) +} diff --git a/service/sign_test.go b/service/sign_test.go new file mode 100644 index 0000000..be5f4b7 --- /dev/null +++ b/service/sign_test.go @@ -0,0 +1,112 @@ +package service + +import ( + "crypto/rand" + "errors" + "io" + "testing" + + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/stretchr/testify/require" +) + +type testSignedDataSrc struct { + e error + d []byte +} + +type testSignedDataReader struct { + SignedDataSource + + e error + d []byte +} + +func testData(t *testing.T, sz int) []byte { + d := make([]byte, sz) + _, err := rand.Read(d) + require.NoError(t, err) + return d +} + +func (s testSignedDataReader) SignedDataSize() int { + return len(s.d) +} + +func (s testSignedDataReader) ReadSignedData(buf []byte) (int, error) { + if s.e != nil { + return 0, s.e + } + + var err error + if len(buf) < len(s.d) { + err = io.ErrUnexpectedEOF + } + return copy(buf, s.d), err +} + +func (s testSignedDataSrc) SignedData() ([]byte, error) { + return s.d, s.e +} + +func TestDataSignature(t *testing.T) { + var err error + + // nil data source + _, err = DataSignature(nil, nil) + require.EqualError(t, err, ErrNilSignedDataSource.Error()) + + // nil private key + _, err = DataSignature(new(testSignedDataSrc), nil) + require.EqualError(t, err, crypto.ErrEmptyPrivateKey.Error()) + + // create test private key + sk := test.DecodeKey(0) + + t.Run("common signed data source", func(t *testing.T) { + // create test data source + src := &testSignedDataSrc{ + d: testData(t, 10), + } + + // create custom error for data source + src.e = errors.New("test error for data source") + + _, err = DataSignature(src, sk) + require.EqualError(t, err, src.e.Error()) + + // reset error to nil + src.e = nil + + // calculate data signature + sig, err := DataSignature(src, sk) + require.NoError(t, err) + + // ascertain that the signature passes verification + require.NoError(t, crypto.Verify(&sk.PublicKey, src.d, sig)) + }) + + t.Run("signed data reader", func(t *testing.T) { + // create test signed data reader + src := &testSignedDataReader{ + d: testData(t, 10), + } + + // create custom error for signed data reader + src.e = errors.New("test error for signed data reader") + + sig, err := DataSignature(src, sk) + require.EqualError(t, err, src.e.Error()) + + // reset error to nil + src.e = nil + + // calculate data signature + sig, err = DataSignature(src, sk) + require.NoError(t, err) + + // ascertain that the signature passes verification + require.NoError(t, crypto.Verify(&sk.PublicKey, src.d, sig)) + }) +} diff --git a/service/types.go b/service/types.go index e7bdcd4..06ea4e2 100644 --- a/service/types.go +++ b/service/types.go @@ -171,3 +171,19 @@ type SessionToken interface { SessionTokenInfo SignatureContainer } + +// SignedDataSource is an interface of the container of a data for signing. +type SignedDataSource interface { + // Must return the required for signature byte slice. + // A non-nil error indicates that the data is not ready for signature. + SignedData() ([]byte, error) +} + +// SignedDataReader is an interface of signed data reader. +type SignedDataReader interface { + // Must return the minimum length of the slice for full reading. + SignedDataSize() int + + // Must behave like Read method of io.Reader and differ only in the reading of the signed data. + ReadSignedData([]byte) (int, error) +}