github.com/TrueCloudLab/frostfs-api-go/v2@v2.0.0-20230228134343-196241c4e79a/util/signature/walletconnect/sign.go (about)

     1  package walletconnect
     2  
     3  import (
     4  	"crypto/ecdsa"
     5  	"crypto/elliptic"
     6  	"crypto/rand"
     7  	"encoding/binary"
     8  	"encoding/hex"
     9  
    10  	crypto "github.com/TrueCloudLab/frostfs-crypto"
    11  )
    12  
    13  const (
    14  	// saltSize is the salt size added to signed message.
    15  	saltSize = 16
    16  	// signatureLen is the length of RFC6979 signature.
    17  	signatureLen = 64
    18  )
    19  
    20  // SignedMessage contains mirrors `SignedMessage` struct from the WalletConnect API.
    21  // https://neon.coz.io/wksdk/core/modules.html#SignedMessage
    22  type SignedMessage struct {
    23  	Data      []byte
    24  	Message   []byte
    25  	PublicKey []byte
    26  	Salt      []byte
    27  }
    28  
    29  // Sign signs message using WalletConnect API. The returned signature
    30  // contains RFC6979 signature and 16-byte salt.
    31  func Sign(p *ecdsa.PrivateKey, msg []byte) ([]byte, error) {
    32  	sm, err := SignMessage(p, msg)
    33  	if err != nil {
    34  		return nil, err
    35  	}
    36  	return append(sm.Data, sm.Salt...), nil
    37  }
    38  
    39  // Verify verifies message using WalletConnect API.
    40  func Verify(p *ecdsa.PublicKey, data, sign []byte) bool {
    41  	if len(sign) != signatureLen+saltSize {
    42  		return false
    43  	}
    44  
    45  	salt := sign[signatureLen:]
    46  	return VerifyMessage(p, SignedMessage{
    47  		Data:    sign[:signatureLen],
    48  		Message: createMessageWithSalt(data, salt),
    49  		Salt:    salt,
    50  	})
    51  }
    52  
    53  // SignMessage signs message with a private key and returns structure similar to
    54  // `signMessage` of the WalletConnect API.
    55  // https://github.com/CityOfZion/wallet-connect-sdk/blob/89c236b/packages/wallet-connect-sdk-core/src/index.ts#L496
    56  // https://github.com/CityOfZion/neon-wallet/blob/1174a9388480e6bbc4f79eb13183c2a573f67ca8/app/context/WalletConnect/helpers.js#L133
    57  func SignMessage(p *ecdsa.PrivateKey, msg []byte) (SignedMessage, error) {
    58  	var salt [saltSize]byte
    59  	_, _ = rand.Read(salt[:])
    60  
    61  	msg = createMessageWithSalt(msg, salt[:])
    62  	sign, err := crypto.SignRFC6979(p, msg)
    63  	if err != nil {
    64  		return SignedMessage{}, err
    65  	}
    66  
    67  	return SignedMessage{
    68  		Data:      sign,
    69  		Message:   msg,
    70  		PublicKey: elliptic.MarshalCompressed(p.Curve, p.X, p.Y),
    71  		Salt:      salt[:],
    72  	}, nil
    73  }
    74  
    75  // VerifyMessage verifies message with a private key and returns structure similar to
    76  // `verifyMessage` of WalletConnect API.
    77  // https://github.com/CityOfZion/wallet-connect-sdk/blob/89c236b/packages/wallet-connect-sdk-core/src/index.ts#L515
    78  // https://github.com/CityOfZion/neon-wallet/blob/1174a9388480e6bbc4f79eb13183c2a573f67ca8/app/context/WalletConnect/helpers.js#L147
    79  func VerifyMessage(p *ecdsa.PublicKey, m SignedMessage) bool {
    80  	if p == nil {
    81  		x, y := elliptic.UnmarshalCompressed(elliptic.P256(), m.PublicKey)
    82  		if x == nil || y == nil {
    83  			return false
    84  		}
    85  		p = &ecdsa.PublicKey{
    86  			Curve: elliptic.P256(),
    87  			X:     x,
    88  			Y:     y,
    89  		}
    90  	}
    91  	return crypto.VerifyRFC6979(p, m.Message, m.Data) == nil
    92  }
    93  
    94  func createMessageWithSalt(msg, salt []byte) []byte {
    95  	// 4 byte prefix + length of the message with salt in bytes +
    96  	// + salt + message + 2 byte postfix.
    97  	saltedLen := hex.EncodedLen(len(salt)) + len(msg)
    98  	data := make([]byte, 4+getVarIntSize(saltedLen)+saltedLen+2)
    99  
   100  	n := copy(data, []byte{0x01, 0x00, 0x01, 0xf0}) // fixed prefix
   101  	n += putVarUint(data[n:], uint64(saltedLen))    // salt is hex encoded, double its size
   102  	n += hex.Encode(data[n:], salt[:])              // for some reason we encode salt in hex
   103  	n += copy(data[n:], msg)
   104  	copy(data[n:], []byte{0x00, 0x00})
   105  
   106  	return data
   107  }
   108  
   109  // Following functions are copied from github.com/nspcc-dev/neo-go/pkg/io package
   110  // to avoid having another dependency.
   111  
   112  // getVarIntSize returns the size in number of bytes of a variable integer.
   113  // Reference: https://github.com/neo-project/neo/blob/26d04a642ac5a1dd1827dabf5602767e0acba25c/src/neo/IO/Helper.cs#L131
   114  func getVarIntSize(value int) int {
   115  	var size uintptr
   116  
   117  	if value < 0xFD {
   118  		size = 1 // unit8
   119  	} else if value <= 0xFFFF {
   120  		size = 3 // byte + uint16
   121  	} else {
   122  		size = 5 // byte + uint32
   123  	}
   124  	return int(size)
   125  }
   126  
   127  // putVarUint puts val in varint form to the pre-allocated buffer.
   128  func putVarUint(data []byte, val uint64) int {
   129  	if val < 0xfd {
   130  		data[0] = byte(val)
   131  		return 1
   132  	}
   133  	if val <= 0xFFFF {
   134  		data[0] = byte(0xfd)
   135  		binary.LittleEndian.PutUint16(data[1:], uint16(val))
   136  		return 3
   137  	}
   138  
   139  	data[0] = byte(0xfe)
   140  	binary.LittleEndian.PutUint32(data[1:], uint32(val))
   141  	return 5
   142  }