From 1feafcbcbf1c7b7ba9181125093e31e4b7dfbf98 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 27 Oct 2021 13:00:11 +0300 Subject: [PATCH] [#53] util: move signature package from neofs-api-go Signed-off-by: Evgenii Stratonikov --- util/signature/data.go | 120 ++++++++++++++++++++++++++++++++++++++ util/signature/options.go | 91 +++++++++++++++++++++++++++++ util/signature/util.go | 31 ++++++++++ 3 files changed, 242 insertions(+) create mode 100644 util/signature/data.go create mode 100644 util/signature/options.go create mode 100644 util/signature/util.go diff --git a/util/signature/data.go b/util/signature/data.go new file mode 100644 index 00000000..783c66ee --- /dev/null +++ b/util/signature/data.go @@ -0,0 +1,120 @@ +package signature + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" +) + +type DataSource interface { + ReadSignedData([]byte) ([]byte, error) + SignedDataSize() int +} + +type DataWithSignature interface { + DataSource + GetSignatureWithKey() (key, sig []byte) + SetSignatureWithKey(key, sig []byte) +} + +type SignOption func(*cfg) + +type KeySignatureHandler func(key []byte, sig []byte) + +type KeySignatureSource func() (key, sig []byte) + +const ( + // PrivateKeyCompressedSize is constant with compressed size of private key (SK). + // D coordinate stored, recover PK by formula x, y = curve.ScalarBaseMul(d,bytes). + PrivateKeyCompressedSize = 32 + + // PublicKeyCompressedSize is constant with compressed size of public key (PK). + PublicKeyCompressedSize = 33 + + // PublicKeyUncompressedSize is constant with uncompressed size of public key (PK). + // First byte always should be 0x4 other 64 bytes is X and Y (32 bytes per coordinate). + // 2 * 32 + 1. + PublicKeyUncompressedSize = 65 +) + +var ( + // ErrEmptyPrivateKey is returned when used private key is empty. + ErrEmptyPrivateKey = errors.New("empty private key") + // ErrInvalidPublicKey is returned when public key cannot be unmarshalled. + ErrInvalidPublicKey = errors.New("invalid public key") + // ErrInvalidSignature is returned if signature cannot be verified. + ErrInvalidSignature = errors.New("invalid signature") +) + +func DataSignature(key *ecdsa.PrivateKey, src DataSource, opts ...SignOption) ([]byte, error) { + if key == nil { + return nil, ErrEmptyPrivateKey + } + + data, err := dataForSignature(src) + if err != nil { + return nil, err + } + defer bytesPool.Put(&data) + + cfg := defaultCfg() + + for i := range opts { + opts[i](cfg) + } + + return cfg.signFunc(key, data) +} + +func SignDataWithHandler(key *ecdsa.PrivateKey, src DataSource, handler KeySignatureHandler, opts ...SignOption) error { + sig, err := DataSignature(key, src, opts...) + if err != nil { + return err + } + + pub := (*keys.PublicKey)(&key.PublicKey) + handler(pub.Bytes(), sig) + + return nil +} + +func VerifyDataWithSource(dataSrc DataSource, sigSrc KeySignatureSource, opts ...SignOption) error { + data, err := dataForSignature(dataSrc) + if err != nil { + return err + } + defer bytesPool.Put(&data) + + cfg := defaultCfg() + + for i := range opts { + opts[i](cfg) + } + + key, sig := sigSrc() + + var pub *keys.PublicKey + if len(key) != 0 { + pub, err = keys.NewPublicKeyFromBytes(key, elliptic.P256()) + if err != nil { + return fmt.Errorf("%w: %v", ErrInvalidPublicKey, err) + } + } + + return cfg.verifyFunc( + (*ecdsa.PublicKey)(pub), + data, + sig, + ) +} + +func SignData(key *ecdsa.PrivateKey, v DataWithSignature, opts ...SignOption) error { + return SignDataWithHandler(key, v, v.SetSignatureWithKey, opts...) +} + +func VerifyData(src DataWithSignature, opts ...SignOption) error { + return VerifyDataWithSource(src, src.GetSignatureWithKey, opts...) +} diff --git a/util/signature/options.go b/util/signature/options.go new file mode 100644 index 00000000..c1cefeed --- /dev/null +++ b/util/signature/options.go @@ -0,0 +1,91 @@ +package signature + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" +) + +var curve = elliptic.P256() + +type cfg struct { + signFunc func(key *ecdsa.PrivateKey, msg []byte) ([]byte, error) + verifyFunc func(key *ecdsa.PublicKey, msg []byte, sig []byte) error +} + +func defaultCfg() *cfg { + return &cfg{ + signFunc: sign, + verifyFunc: verify, + } +} + +func sign(key *ecdsa.PrivateKey, msg []byte) ([]byte, error) { + h := sha512.Sum512(msg) + x, y, err := ecdsa.Sign(rand.Reader, key, h[:]) + if err != nil { + return nil, err + } + return elliptic.Marshal(elliptic.P256(), x, y), nil +} + +func verify(key *ecdsa.PublicKey, msg []byte, sig []byte) error { + h := sha512.Sum512(msg) + r, s := unmarshalXY(sig) + if r != nil && s != nil && ecdsa.Verify(key, h[:], r, s) { + return nil + } + return ErrInvalidSignature +} + +// unmarshalXY converts a point, serialized by Marshal, into an x, y pair. +// It is an error if the point is not in uncompressed form. +// On error, x,y = nil. +// Unlike the original version of the code, we ignore that x or y not on the curve +// -------------- +// It's copy-paste elliptic.Unmarshal(curve, data) stdlib function, without last line +// of code. +// Link - https://golang.org/pkg/crypto/elliptic/#Unmarshal +func unmarshalXY(data []byte) (x *big.Int, y *big.Int) { + if len(data) != PublicKeyUncompressedSize { + return + } else if data[0] != 4 { // uncompressed form + return + } + + p := curve.Params().P + x = new(big.Int).SetBytes(data[1:PublicKeyCompressedSize]) + y = new(big.Int).SetBytes(data[PublicKeyCompressedSize:]) + + if x.Cmp(p) >= 0 || y.Cmp(p) >= 0 { + x, y = nil, nil + } + + return +} + +func SignWithRFC6979() SignOption { + return func(c *cfg) { + c.signFunc = signRFC6979 + c.verifyFunc = verifyRFC6979 + } +} + +func signRFC6979(key *ecdsa.PrivateKey, msg []byte) ([]byte, error) { + p := &keys.PrivateKey{PrivateKey: *key} + return p.Sign(msg), nil +} + +func verifyRFC6979(key *ecdsa.PublicKey, msg []byte, sig []byte) error { + p := (*keys.PublicKey)(key) + h := sha256.Sum256(msg) + if p.Verify(sig, h[:]) { + return nil + } + return ErrInvalidSignature +} diff --git a/util/signature/util.go b/util/signature/util.go new file mode 100644 index 00000000..f9aef100 --- /dev/null +++ b/util/signature/util.go @@ -0,0 +1,31 @@ +package signature + +import ( + "errors" + "sync" +) + +var bytesPool = sync.Pool{ + New: func() interface{} { + b := make([]byte, 5<<20) + return &b + }, +} + +func dataForSignature(src DataSource) ([]byte, error) { + if src == nil { + return nil, errors.New("nil source") + } + + buf := *bytesPool.Get().(*[]byte) + + if size := src.SignedDataSize(); size < 0 { + return nil, errors.New("negative length") + } else if size <= cap(buf) { + buf = buf[:size] + } else { + buf = make([]byte, size) + } + + return src.ReadSignedData(buf) +}