[#386] util/signature: Add WalletConnect API support
To avoid introducing new dependency (neo-go), crypto routines are used as in other code. Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
parent
f9a91e5f33
commit
dd233c3fbc
6 changed files with 322 additions and 0 deletions
BIN
refs/grpc/types.pb.go
generated
BIN
refs/grpc/types.pb.go
generated
Binary file not shown.
|
@ -37,6 +37,7 @@ type SignatureScheme uint32
|
||||||
const (
|
const (
|
||||||
ECDSA_SHA512 SignatureScheme = iota
|
ECDSA_SHA512 SignatureScheme = iota
|
||||||
ECDSA_RFC6979_SHA256
|
ECDSA_RFC6979_SHA256
|
||||||
|
ECDSA_RFC6979_SHA256_WALLET_CONNECT
|
||||||
)
|
)
|
||||||
|
|
||||||
type Signature struct {
|
type Signature struct {
|
||||||
|
|
|
@ -2,9 +2,11 @@ package signature
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/util/signature/walletconnect"
|
||||||
crypto "github.com/nspcc-dev/neofs-crypto"
|
crypto "github.com/nspcc-dev/neofs-crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,12 +26,22 @@ func verify(cfg *cfg, data []byte, sig *refs.Signature) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub := crypto.UnmarshalPublicKey(sig.GetKey())
|
pub := crypto.UnmarshalPublicKey(sig.GetKey())
|
||||||
|
if pub == nil {
|
||||||
|
return crypto.ErrEmptyPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
switch cfg.scheme {
|
switch cfg.scheme {
|
||||||
case refs.ECDSA_SHA512:
|
case refs.ECDSA_SHA512:
|
||||||
return crypto.Verify(pub, data, sig.GetSign())
|
return crypto.Verify(pub, data, sig.GetSign())
|
||||||
case refs.ECDSA_RFC6979_SHA256:
|
case refs.ECDSA_RFC6979_SHA256:
|
||||||
return crypto.VerifyRFC6979(pub, data, sig.GetSign())
|
return crypto.VerifyRFC6979(pub, data, sig.GetSign())
|
||||||
|
case refs.ECDSA_RFC6979_SHA256_WALLET_CONNECT:
|
||||||
|
buf := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
|
||||||
|
base64.StdEncoding.Encode(buf, data)
|
||||||
|
if !walletconnect.Verify(pub, buf, sig.GetSign()) {
|
||||||
|
return crypto.ErrInvalidSignature
|
||||||
|
}
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported signature scheme %s", cfg.scheme)
|
return fmt.Errorf("unsupported signature scheme %s", cfg.scheme)
|
||||||
}
|
}
|
||||||
|
@ -41,6 +53,10 @@ func sign(cfg *cfg, key *ecdsa.PrivateKey, data []byte) ([]byte, error) {
|
||||||
return crypto.Sign(key, data)
|
return crypto.Sign(key, data)
|
||||||
case refs.ECDSA_RFC6979_SHA256:
|
case refs.ECDSA_RFC6979_SHA256:
|
||||||
return crypto.SignRFC6979(key, data)
|
return crypto.SignRFC6979(key, data)
|
||||||
|
case refs.ECDSA_RFC6979_SHA256_WALLET_CONNECT:
|
||||||
|
buf := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
|
||||||
|
base64.StdEncoding.Encode(buf, data)
|
||||||
|
return walletconnect.Sign(key, buf)
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unsupported scheme %s", cfg.scheme))
|
panic(fmt.Sprintf("unsupported scheme %s", cfg.scheme))
|
||||||
}
|
}
|
||||||
|
@ -59,3 +75,9 @@ func WithBuffer(buf []byte) SignOption {
|
||||||
c.buffer = buf
|
c.buffer = buf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SignWithWalletConnect() SignOption {
|
||||||
|
return func(c *cfg) {
|
||||||
|
c.scheme = refs.ECDSA_RFC6979_SHA256_WALLET_CONNECT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
44
util/signature/sign_test.go
Normal file
44
util/signature/sign_test.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package signature
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testData struct {
|
||||||
|
data []byte
|
||||||
|
sig *refs.Signature
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t testData) SignedDataSize() int { return len(t.data) }
|
||||||
|
func (t testData) ReadSignedData(data []byte) ([]byte, error) {
|
||||||
|
n := copy(data, t.data)
|
||||||
|
return data[:n], nil
|
||||||
|
}
|
||||||
|
func (t testData) GetSignature() *refs.Signature { return t.sig }
|
||||||
|
func (t *testData) SetSignature(s *refs.Signature) { t.sig = s }
|
||||||
|
|
||||||
|
func TestWalletConnect(t *testing.T) {
|
||||||
|
testCases := [...][]byte{
|
||||||
|
{},
|
||||||
|
{0},
|
||||||
|
{1, 2},
|
||||||
|
{3, 4, 5},
|
||||||
|
{6, 7, 8, 9, 10, 11, 12},
|
||||||
|
}
|
||||||
|
|
||||||
|
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
td := &testData{data: tc}
|
||||||
|
require.NoError(t, SignData(pk, td, SignWithWalletConnect()))
|
||||||
|
require.Equal(t, refs.ECDSA_RFC6979_SHA256_WALLET_CONNECT, td.sig.GetScheme())
|
||||||
|
require.NoError(t, VerifyData(td))
|
||||||
|
}
|
||||||
|
}
|
142
util/signature/walletconnect/sign.go
Normal file
142
util/signature/walletconnect/sign.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
package walletconnect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
crypto "github.com/nspcc-dev/neofs-crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// saltSize is the salt size added to signed message.
|
||||||
|
saltSize = 16
|
||||||
|
// signatureLen is the length of RFC6979 signature.
|
||||||
|
signatureLen = 64
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignedMessage contains mirrors `SignedMessage` struct from the WalletConnect API.
|
||||||
|
// https://neon.coz.io/wksdk/core/modules.html#SignedMessage
|
||||||
|
type SignedMessage struct {
|
||||||
|
Data []byte
|
||||||
|
Message []byte
|
||||||
|
PublicKey []byte
|
||||||
|
Salt []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs message using WalletConnect API. The returned signature
|
||||||
|
// contains RFC6979 signature and 16-byte salt.
|
||||||
|
func Sign(p *ecdsa.PrivateKey, msg []byte) ([]byte, error) {
|
||||||
|
sm, err := SignMessage(p, msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return append(sm.Data, sm.Salt...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify verifies message using WalletConnect API.
|
||||||
|
func Verify(p *ecdsa.PublicKey, data, sign []byte) bool {
|
||||||
|
if len(sign) != signatureLen+saltSize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
salt := sign[signatureLen:]
|
||||||
|
return VerifyMessage(p, SignedMessage{
|
||||||
|
Data: sign[:signatureLen],
|
||||||
|
Message: createMessageWithSalt(data, salt),
|
||||||
|
Salt: salt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignMessage signs message with a private key and returns structure similar to
|
||||||
|
// `signMessage` of the WalletConnect API.
|
||||||
|
// https://github.com/CityOfZion/wallet-connect-sdk/blob/89c236b/packages/wallet-connect-sdk-core/src/index.ts#L496
|
||||||
|
// https://github.com/CityOfZion/neon-wallet/blob/1174a9388480e6bbc4f79eb13183c2a573f67ca8/app/context/WalletConnect/helpers.js#L133
|
||||||
|
func SignMessage(p *ecdsa.PrivateKey, msg []byte) (SignedMessage, error) {
|
||||||
|
var salt [saltSize]byte
|
||||||
|
_, _ = rand.Read(salt[:])
|
||||||
|
|
||||||
|
msg = createMessageWithSalt(msg, salt[:])
|
||||||
|
sign, err := crypto.SignRFC6979(p, msg)
|
||||||
|
if err != nil {
|
||||||
|
return SignedMessage{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return SignedMessage{
|
||||||
|
Data: sign,
|
||||||
|
Message: msg,
|
||||||
|
PublicKey: elliptic.MarshalCompressed(p.Curve, p.X, p.Y),
|
||||||
|
Salt: salt[:],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyMessage verifies message with a private key and returns structure similar to
|
||||||
|
// `verifyMessage` of WalletConnect API.
|
||||||
|
// https://github.com/CityOfZion/wallet-connect-sdk/blob/89c236b/packages/wallet-connect-sdk-core/src/index.ts#L515
|
||||||
|
// https://github.com/CityOfZion/neon-wallet/blob/1174a9388480e6bbc4f79eb13183c2a573f67ca8/app/context/WalletConnect/helpers.js#L147
|
||||||
|
func VerifyMessage(p *ecdsa.PublicKey, m SignedMessage) bool {
|
||||||
|
if p == nil {
|
||||||
|
x, y := elliptic.UnmarshalCompressed(elliptic.P256(), m.PublicKey)
|
||||||
|
if x == nil || y == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
p = &ecdsa.PublicKey{
|
||||||
|
Curve: elliptic.P256(),
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crypto.VerifyRFC6979(p, m.Message, m.Data) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMessageWithSalt(msg, salt []byte) []byte {
|
||||||
|
// 4 byte prefix + length of the message with salt in bytes +
|
||||||
|
// + salt + message + 2 byte postfix.
|
||||||
|
saltedLen := hex.EncodedLen(len(salt)) + len(msg)
|
||||||
|
data := make([]byte, 4+getVarIntSize(saltedLen)+saltedLen+2)
|
||||||
|
|
||||||
|
n := copy(data, []byte{0x01, 0x00, 0x01, 0xf0}) // fixed prefix
|
||||||
|
n += putVarUint(data[n:], uint64(saltedLen)) // salt is hex encoded, double its size
|
||||||
|
n += hex.Encode(data[n:], salt[:]) // for some reason we encode salt in hex
|
||||||
|
n += copy(data[n:], msg)
|
||||||
|
copy(data[n:], []byte{0x00, 0x00})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Following functions are copied from github.com/nspcc-dev/neo-go/pkg/io package
|
||||||
|
// to avoid having another dependency.
|
||||||
|
|
||||||
|
// getVarIntSize returns the size in number of bytes of a variable integer.
|
||||||
|
// Reference: https://github.com/neo-project/neo/blob/26d04a642ac5a1dd1827dabf5602767e0acba25c/src/neo/IO/Helper.cs#L131
|
||||||
|
func getVarIntSize(value int) int {
|
||||||
|
var size uintptr
|
||||||
|
|
||||||
|
if value < 0xFD {
|
||||||
|
size = 1 // unit8
|
||||||
|
} else if value <= 0xFFFF {
|
||||||
|
size = 3 // byte + uint16
|
||||||
|
} else {
|
||||||
|
size = 5 // byte + uint32
|
||||||
|
}
|
||||||
|
return int(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// putVarUint puts val in varint form to the pre-allocated buffer.
|
||||||
|
func putVarUint(data []byte, val uint64) int {
|
||||||
|
if val < 0xfd {
|
||||||
|
data[0] = byte(val)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if val <= 0xFFFF {
|
||||||
|
data[0] = byte(0xfd)
|
||||||
|
binary.LittleEndian.PutUint16(data[1:], uint16(val))
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
data[0] = byte(0xfe)
|
||||||
|
binary.LittleEndian.PutUint32(data[1:], uint32(val))
|
||||||
|
return 5
|
||||||
|
}
|
113
util/signature/walletconnect/sign_test.go
Normal file
113
util/signature/walletconnect/sign_test.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package walletconnect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSignMessage(t *testing.T) {
|
||||||
|
p1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
msg := []byte("NEO")
|
||||||
|
result, err := SignMessage(p1, msg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, elliptic.MarshalCompressed(elliptic.P256(), p1.PublicKey.X, p1.PublicKey.Y), result.PublicKey)
|
||||||
|
require.Equal(t, saltSize, len(result.Salt))
|
||||||
|
require.Equal(t, 64, len(result.Data))
|
||||||
|
require.Equal(t, 4+1+16*2+3+2, len(result.Message))
|
||||||
|
|
||||||
|
require.True(t, VerifyMessage(&p1.PublicKey, result))
|
||||||
|
|
||||||
|
t.Run("invalid public key", func(t *testing.T) {
|
||||||
|
p2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, VerifyMessage(&p2.PublicKey, result))
|
||||||
|
})
|
||||||
|
t.Run("invalid signature", func(t *testing.T) {
|
||||||
|
result := result
|
||||||
|
result.Data[0] ^= 0xFF
|
||||||
|
require.False(t, VerifyMessage(&p1.PublicKey, result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSign(t *testing.T) {
|
||||||
|
p1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
msg := []byte("NEO")
|
||||||
|
sign, err := Sign(p1, msg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, Verify(&p1.PublicKey, msg, sign))
|
||||||
|
|
||||||
|
t.Run("invalid public key", func(t *testing.T) {
|
||||||
|
p2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, Verify(&p2.PublicKey, msg, sign))
|
||||||
|
})
|
||||||
|
t.Run("invalid signature", func(t *testing.T) {
|
||||||
|
sign[0] ^= 0xFF
|
||||||
|
require.False(t, Verify(&p1.PublicKey, msg, sign))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyNeonWallet(t *testing.T) {
|
||||||
|
testCases := [...]struct {
|
||||||
|
publicKey string
|
||||||
|
data string
|
||||||
|
salt string
|
||||||
|
messageHex string
|
||||||
|
messageOriginal string
|
||||||
|
}{
|
||||||
|
{ // Test values from this GIF https://github.com/CityOfZion/neon-wallet/pull/2390 .
|
||||||
|
publicKey: "02ce6228ba2cb2fc235be93aff9cd5fc0851702eb9791552f60db062f01e3d83f6",
|
||||||
|
data: "90ab1886ca0bece59b982d9ade8f5598065d651362fb9ce45ad66d0474b89c0b80913c8f0118a282acbdf200a429ba2d81bc52534a53ab41a2c6dfe2f0b4fb1b",
|
||||||
|
salt: "d41e348afccc2f3ee45cd9f5128b16dc",
|
||||||
|
messageHex: "010001f05c6434316533343861666363633266336565343563643966353132386231366463436172616c686f2c206d756c65712c206f2062616775697520656820697373756d65726d6f2074616978206c696761646f206e61206d697373e36f3f0000",
|
||||||
|
messageOriginal: "436172616c686f2c206d756c65712c206f2062616775697520656820697373756d65726d6f2074616978206c696761646f206e61206d697373e36f3f",
|
||||||
|
},
|
||||||
|
{ // Test value from wallet connect integration test
|
||||||
|
publicKey: "03bd9108c0b49f657e9eee50d1399022bd1e436118e5b7529a1b7cd606652f578f",
|
||||||
|
data: "510caa8cb6db5dedf04d215a064208d64be7496916d890df59aee132db8f2b07532e06f7ea664c4a99e3bcb74b43a35eb9653891b5f8701d2aef9e7526703eaa",
|
||||||
|
salt: "2c5b189569e92cce12e1c640f23e83ba",
|
||||||
|
messageHex: "010001f02632633562313839353639653932636365313265316336343066323365383362613132333435360000",
|
||||||
|
messageOriginal: "313233343536", // ascii string "123456"
|
||||||
|
},
|
||||||
|
{ // Test value from wallet connect integration test
|
||||||
|
publicKey: "03bd9108c0b49f657e9eee50d1399022bd1e436118e5b7529a1b7cd606652f578f",
|
||||||
|
data: "1e13f248962d8b3b60708b55ddf448d6d6a28c6b43887212a38b00bf6bab695e61261e54451c6e3d5f1f000e5534d166c7ca30f662a296d3a9aafa6d8c173c01",
|
||||||
|
salt: "58c86b2e74215b4f36b47d731236be3b",
|
||||||
|
messageHex: "010001f02035386338366232653734323135623466333662343764373331323336626533620000",
|
||||||
|
messageOriginal: "", // empty string
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
rawPub, err := hex.DecodeString(testCase.publicKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
data, err := hex.DecodeString(testCase.data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
salt, err := hex.DecodeString(testCase.salt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
msg, err := hex.DecodeString(testCase.messageHex)
|
||||||
|
require.NoError(t, err)
|
||||||
|
orig, err := hex.DecodeString(testCase.messageOriginal)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, msg, createMessageWithSalt(orig, salt))
|
||||||
|
|
||||||
|
sm := SignedMessage{
|
||||||
|
Data: data,
|
||||||
|
Message: msg,
|
||||||
|
PublicKey: rawPub,
|
||||||
|
Salt: salt,
|
||||||
|
}
|
||||||
|
require.True(t, VerifyMessage(nil, sm))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue