Skip to content

Commit

Permalink
feat(unchecked-extrinsic): ecdsa signature verification (#303)
Browse files Browse the repository at this point in the history
  • Loading branch information
failfmi authored Nov 27, 2023
1 parent 9f96950 commit f4937b4
Show file tree
Hide file tree
Showing 10 changed files with 546 additions and 14 deletions.
Binary file modified build/runtime.wasm
Binary file not shown.
46 changes: 36 additions & 10 deletions execution/types/unchecked_extrinsic.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package types

import (
"bytes"
"fmt"
"reflect"

sc "github.com/LimeChain/goscale"
"github.com/LimeChain/gosemble/primitives/io"
Expand Down Expand Up @@ -138,28 +140,27 @@ func (uxt uncheckedExtrinsic) verify(signature primitives.MultiSignature, msg sc
if signature.IsEd25519() {
sigEd25519, err := signature.AsEd25519()
if err != nil {
// TODO: return err
log.Critical(err.Error())
}
sigBytes := sc.FixedSequenceU8ToBytes(sigEd25519.FixedSequence)
return uxt.crypto.Ed25519Verify(sigBytes, msgBytes, signerBytes)
} else if signature.IsSr25519() {

sigSr25519, err := signature.AsSr25519()
if err != nil {
// TODO: return err
log.Critical(err.Error())
}
sigBytes := sc.FixedSequenceU8ToBytes(sigSr25519.FixedSequence)
return uxt.crypto.Sr25519Verify(sigBytes, msgBytes, signerBytes)
} else if signature.IsEcdsa() {
return true
// TODO:
// let m = sp_io::hashing::blake2_256(msg.get());
// match sp_io::crypto::secp256k1_ecdsa_recover_compressed(sig.as_ref(), &m) {
// Ok(pubkey) =>
// &sp_io::hashing::blake2_256(pubkey.as_ref()) ==
// <dyn AsRef<[u8; 32]>>::as_ref(who),
// _ => false,
// }
sigEcdsa, err := signature.AsEcdsa()
if err != nil {
// TODO: return err
log.Critical(err.Error())
}

return uxt.verifyEcdsa(sigEcdsa, msgBytes, signerBytes)
}

log.Critical("invalid MultiSignature type in Verify")
Expand All @@ -177,3 +178,28 @@ func (uxt uncheckedExtrinsic) usingEncoded(sp primitives.SignedPayload) sc.Seque
return sc.BytesToSequenceU8(enc)
}
}

func (uxt uncheckedExtrinsic) verifyEcdsa(signature primitives.SignatureEcdsa, msgBytes []byte, signer []byte) bool {
sigBytes := sc.FixedSequenceU8ToBytes(signature.FixedSequence)
msg := uxt.hashing.Blake256(msgBytes)

// This returns either the 33-byte ECDSA Public Key or an error.
recovered := uxt.crypto.EcdsaRecoverCompressed(sigBytes, msg)
buffer := bytes.NewBuffer(recovered)

result, err := sc.DecodeResult(buffer, primitives.DecodeEcdsaPublicKey, primitives.DecodeEcdsaVerifyError)
if err != nil {
// TODO: return err
log.Critical(err.Error())
}

if result.HasError {
log.Debug(fmt.Sprintf("Failed to verify signature. Error: [%s]", result.Value.(error).Error()))
return false
}

// In order to match AccountId, ECDSA public keys are hashed to 32 bytes.
hashPublicKey := uxt.hashing.Blake256(result.Value.Bytes())

return reflect.DeepEqual(hashPublicKey, signer)
}
108 changes: 105 additions & 3 deletions execution/types/unchecked_extrinsic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,27 @@ var (
types.NewInvalidTransactionBadProof(),
)

signerAddressBytes = []byte{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
}
signerAddressBytes = make([]byte, 32)
signer25519Address, _ = types.NewEd25519PublicKey(sc.BytesToSequenceU8(signerAddressBytes)...)
signerAccountId = types.NewAccountId[types.PublicKey](signer25519Address)
signer = types.NewMultiAddressId(signerAccountId)

ecdsaAddressBytes = make([]byte, 33)
ecdsaPublicKey, _ = types.NewEcdsaPublicKey(sc.BytesToSequenceU8(ecdsaAddressBytes)...)

signatureBytes = []byte{
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1,
}
ecdsaSignatureBytes = []byte{
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
}

signatureEd25519 = types.NewMultiSignatureEd25519(
types.NewSignatureEd25519(
sc.BytesToFixedSequenceU8(signatureBytes)...,
Expand All @@ -51,6 +59,12 @@ var (
),
)

signatureEcdsa = types.NewMultiSignatureEcdsa(
types.NewSignatureEcdsa(
sc.BytesToFixedSequenceU8(ecdsaSignatureBytes)...,
),
)

unknownMultisignature = types.MultiSignature{
VaryingData: sc.NewVaryingData(sc.U8(3), signatureEd25519),
}
Expand Down Expand Up @@ -334,6 +348,94 @@ func Test_Check_SignedUncheckedExtrinsic_Success_Sr25519(t *testing.T) {
mockCrypto.AssertCalled(t, "Sr25519Verify", signatureBytes, encodedPayloadBytes, signerAddressBytes)
}

func Test_SignedUncheckedExtrinsic_Check_Ecdsa_Success(t *testing.T) {
setup(signatureEcdsa)
recoverResult := sc.Result[sc.Encodable]{
HasError: false,
Value: ecdsaPublicKey,
}
expect := NewCheckedExtrinsic(sc.NewOption[types.AccountId[types.PublicKey]](signerAccountId), mockCall, mockSignedExtra).(checkedExtrinsic)

mocksSignedPayload.On("Bytes").Return(encodedPayloadBytes)
mockHashing.On("Blake256", encodedPayloadBytes).Return(encodedPayloadBytes)
mockCrypto.On("EcdsaRecoverCompressed", ecdsaSignatureBytes, encodedPayloadBytes).Return(recoverResult.Bytes())
mockHashing.On("Blake256", ecdsaAddressBytes).Return(signerAddressBytes)

result, err := targetSigned.Check()

assert.Nil(t, err)
checked := result.(checkedExtrinsic)
assert.Equal(t, expect.extra, checked.extra)
assert.Equal(t, expect.signer, checked.signer)
assert.Equal(t, expect.function, checked.function)

mocksSignedPayload.AssertCalled(t, "Bytes")
mockHashing.AssertCalled(t, "Blake256", encodedPayloadBytes)
mockCrypto.AssertCalled(t, "EcdsaRecoverCompressed", ecdsaSignatureBytes, encodedPayloadBytes)
mockHashing.AssertCalled(t, "Blake256", ecdsaAddressBytes)
}

func Test_SignedUncheckedExtrinsic_Check_Ecdsa_BadProof_MismatchingAddresses(t *testing.T) {
setup(signatureEcdsa)
recoverResult := sc.Result[sc.Encodable]{
HasError: false,
Value: ecdsaPublicKey,
}

mocksSignedPayload.On("Bytes").Return(encodedPayloadBytes)
mockHashing.On("Blake256", encodedPayloadBytes).Return(encodedPayloadBytes)
mockCrypto.On("EcdsaRecoverCompressed", ecdsaSignatureBytes, encodedPayloadBytes).Return(recoverResult.Bytes())
mockHashing.On("Blake256", ecdsaAddressBytes).Return(ecdsaAddressBytes) // Set invalid address

result, err := targetSigned.Check()

assert.Nil(t, result)
assert.Equal(t, invalidTransactionBadProofError, err)

mocksSignedPayload.AssertCalled(t, "Bytes")
mockHashing.AssertCalled(t, "Blake256", encodedPayloadBytes)
mockCrypto.AssertCalled(t, "EcdsaRecoverCompressed", ecdsaSignatureBytes, encodedPayloadBytes)
mockHashing.AssertCalled(t, "Blake256", ecdsaAddressBytes)
}

func Test_SignedUncheckedExtrinsic_Check_Ecdsa_BadProof_BadSignature(t *testing.T) {
setup(signatureEcdsa)
recoverResult := sc.Result[sc.Encodable]{
HasError: true,
Value: types.NewEcdsaVerifyErrorBadSignature(),
}

mocksSignedPayload.On("Bytes").Return(encodedPayloadBytes)
mockHashing.On("Blake256", encodedPayloadBytes).Return(encodedPayloadBytes)
mockCrypto.On("EcdsaRecoverCompressed", ecdsaSignatureBytes, encodedPayloadBytes).Return(recoverResult.Bytes())

result, err := targetSigned.Check()

assert.Nil(t, result)
assert.Equal(t, invalidTransactionBadProofError, err)

mocksSignedPayload.AssertCalled(t, "Bytes")
mockHashing.AssertCalled(t, "Blake256", encodedPayloadBytes)
mockCrypto.AssertCalled(t, "EcdsaRecoverCompressed", ecdsaSignatureBytes, encodedPayloadBytes)
}

func Test_SignedUncheckedExtrinsic_Check_Ecdsa_Panics_InvalidResultBytes(t *testing.T) {
setup(signatureEcdsa)
recoverResult := sc.U8(5)

mocksSignedPayload.On("Bytes").Return(encodedPayloadBytes)
mockHashing.On("Blake256", encodedPayloadBytes).Return(encodedPayloadBytes)
mockCrypto.On("EcdsaRecoverCompressed", ecdsaSignatureBytes, encodedPayloadBytes).Return(recoverResult.Bytes())

assert.PanicsWithValue(t, "invalid bool representation", func() {
targetSigned.Check()
})

mocksSignedPayload.AssertCalled(t, "Bytes")
mockHashing.AssertCalled(t, "Blake256", encodedPayloadBytes)
mockCrypto.AssertCalled(t, "EcdsaRecoverCompressed", ecdsaSignatureBytes, encodedPayloadBytes)
}

func Test_Check_SignedUncheckedExtrinsic_UnknownSignatureType(t *testing.T) {
setup(unknownMultisignature)

Expand Down
2 changes: 1 addition & 1 deletion goscale
5 changes: 5 additions & 0 deletions mocks/io_crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ func (m *IoCrypto) EcdsaGenerate(keyTypeId []byte, seed []byte) []byte {
return args.Get(0).([]byte)
}

func (m *IoCrypto) EcdsaRecoverCompressed(signature []byte, msg []byte) []byte {
args := m.Called(signature, msg)
return args.Get(0).([]byte)
}

func (m *IoCrypto) Ed25519Generate(keyTypeId []byte, seed []byte) []byte {
args := m.Called(keyTypeId, seed)

Expand Down
14 changes: 14 additions & 0 deletions primitives/io/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

type Crypto interface {
EcdsaGenerate(keyTypeId []byte, seed []byte) []byte
EcdsaRecoverCompressed(signature []byte, msg []byte) []byte

Ed25519Generate(keyTypeId []byte, seed []byte) []byte
Ed25519Verify(signature []byte, message []byte, pubKey []byte) bool
Expand All @@ -32,6 +33,19 @@ func (c crypto) EcdsaGenerate(keyTypeId []byte, seed []byte) []byte {
//return c.memoryTranslator.ToWasmMemorySlice(r, 32)
}

func (c crypto) EcdsaRecoverCompressed(signature []byte, msg []byte) []byte {
sigOffsetSize := c.memoryTranslator.BytesToOffsetAndSize(signature)
sigOffset, _ := c.memoryTranslator.Int64ToOffsetAndSize(sigOffsetSize) // signature: 65-byte

msgOffsetSize := c.memoryTranslator.BytesToOffsetAndSize(msg)
msgOffset, _ := c.memoryTranslator.Int64ToOffsetAndSize(msgOffsetSize) // message: 32-byte

r := env.ExtCryptoSecp256k1EcdsaRecoverCompressedVersion2(sigOffset, msgOffset)
offset, size := c.memoryTranslator.Int64ToOffsetAndSize(r)

return c.memoryTranslator.GetWasmMemorySlice(offset, size)
}

func (c crypto) Ed25519Generate(keyTypeId []byte, seed []byte) []byte {
r := env.ExtCryptoEd25519GenerateVersion1(
c.memoryTranslator.Offset32(keyTypeId),
Expand Down
73 changes: 73 additions & 0 deletions primitives/types/ecdsa_verify_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package types

import (
"bytes"

sc "github.com/LimeChain/goscale"
)

const (
EcdsaVerifyErrorBadRS sc.U8 = iota
EcdsaVerifyErrorBadV
EcdsaVerifyErrorBadSignature
)

func NewEcdsaVerifyErrorBadRS() EcdsaVerifyError {
return EcdsaVerifyError(sc.NewVaryingData(EcdsaVerifyErrorBadRS))
}

func NewEcdsaVerifyErrorBadV() EcdsaVerifyError {
return EcdsaVerifyError(sc.NewVaryingData(EcdsaVerifyErrorBadV))
}

func NewEcdsaVerifyErrorBadSignature() EcdsaVerifyError {
return EcdsaVerifyError(sc.NewVaryingData(EcdsaVerifyErrorBadSignature))
}

type EcdsaVerifyError sc.VaryingData

func (err EcdsaVerifyError) Encode(buffer *bytes.Buffer) error {
if len(err) == 0 {
return newTypeError("EcdsaVerifyError")
}
return err[0].Encode(buffer)
}

func DecodeEcdsaVerifyError(buffer *bytes.Buffer) (EcdsaVerifyError, error) {
b, err := sc.DecodeU8(buffer)
if err != nil {
return nil, err
}

switch b {
case EcdsaVerifyErrorBadRS:
return NewEcdsaVerifyErrorBadRS(), nil
case EcdsaVerifyErrorBadV:
return NewEcdsaVerifyErrorBadV(), nil
case EcdsaVerifyErrorBadSignature:
return NewEcdsaVerifyErrorBadSignature(), nil
default:
return nil, newTypeError("EcdsaVerifyError")
}
}

func (err EcdsaVerifyError) Bytes() []byte {
return sc.EncodedBytes(err)
}

func (err EcdsaVerifyError) Error() string {
if len(err) == 0 {
return newTypeError("EcdsaVerifyError").Error()
}

switch err[0] {
case EcdsaVerifyErrorBadRS:
return "Bad RS"
case EcdsaVerifyErrorBadV:
return "Bad V"
case EcdsaVerifyErrorBadSignature:
return "Bad signature"
default:
return newTypeError("EcdsaVerifyError").Error()
}
}
Loading

0 comments on commit f4937b4

Please sign in to comment.