chainapsis / keplr-wallet

The most powerful wallet for the Cosmos ecosystem and the Interchain
https://www.keplr.app
Other
774 stars 462 forks source link

[QUERY] extraction of publicKey from recoverable signature produced from `signEthereum()` #680

Open arnabghose997 opened 1 year ago

arnabghose997 commented 1 year ago

I am working on an App which signs a particular message using the signEthereum() method https://github.com/chainapsis/keplr-wallet/blob/9ce18e3737b3765c6136b8bf0413c5528e644a78/packages/provider/src/core.ts#L223 and the signature is sent to a Tendermint-based blockchain for verification through RPC

On the blockchain end, we are using the go-ethereum library to extract the public key from the signature sent from the App. We have used Kekkac256 as well as Sha256 to hash the signData. However, the extracted address (from the extracted public key) doesn't seem to match the expected bech32 address.

The question I have is, whether signEthereum encapsulates the message in ADR-036 SignDOC (https://docs.cosmos.network/v0.47/architecture/adr-036-arbitrary-signature) before signing it, or does it handles the message similar to personal_sign spec (https://docs.metamask.io/guide/signing-data.html#personal-sign) as utilised by Metamask?

Thunnini commented 1 year ago

Actually, it's PR'd by the EVMOS team and managed by them, so I don't know.

But I think it's compatible with ethereum. And when you use signEthereum, it looks like ADR-036, it's just because they recycled the ADR-036 page. It doesn't actually change the sign data to ADR-036. As far as I know, in ethereum, the signing data is hashed with keccak256. And if you're successful in getting the public key, that public key should be in uncompressed format, and you should drop the prefix byte, hash it with keccak256, use the last 20 bytes, encode it with bech32, and you should get the corresponding bech32 address.

Can you confirm if it doesn't work?

arnabghose997 commented 1 year ago

Thank you @Thunnini for the response. I will let you know here. I have just one question.

When you say that signData is hashed using keccak256, do you mean it is hashed in the following way? :

keccak256("\x19Ethereum Signed Message:\n"${len(signData)}${signData}).

Or, is it something like following, where the hashing of singData happens without any padding like above:

keccak256(signData).
Thunnini commented 1 year ago

I think it will be keccak256("\x19Ethereum Signed Message:\n"${len(signData)}${signData}) if EthSignType.MESSAGE is used. Referring to the code, this is the intended behavior for the code.

arnabghose997 commented 1 year ago

@Thunnini I tried the approach you suggested, but I wasn't able to recover the correct bech32 address. Following is the code for both client and server for the reference.

client.js:

const msgToSign = "hi";
const keplrAddress = "osmo1f6r0x3pljpl7pe76zzv36l0ksztqmdltakhv4r";
const keplrChainId  = "osmosis-1"
const ethType = "message";

const signatureBytes = await window.keplr.requestMethod("signEthereum", [
  keplrChainId,
  keplrAddress,
  msgToSign,
  ethType,
]);

const signatureB64 = Buffer.from(signatureBytes).toString("base64");

console.log("Singature: ", signatureB64); // Will copy it to be used in server.go

server.go:

import (
  "fmt"
  "encoding/base64"

  etheraccounts "github.com/ethereum/go-ethereum/accounts"
  ethercrypto "github.com/ethereum/go-ethereum/crypto"
  "github.com/cosmos/btcutil/bech32"
  "golang.org/x/crypto/sha3"
)

func main() {
    // Hash the message
    msg := "hi"
    msgHash, _ := etheraccounts.TextAndHash([]byte(msg))

    // Decode base54-encoded signature string to bytes
    signature := "bSxtpN1hzN4rsafwcmD7DsLMfTVpQpvMz1CmK1s2uDkplsZXnUrmi092yn35/edVKCU/nxZ7VDBB/irjyFBDPBs=" // copied from client.js
    signatureBytes, err := base64.StdEncoding.DecodeString(signature)
    if err != nil {
      return err
    }

    // Handle the signature recieved from web3-js client package by subtracting 27 from the recovery byte
    if signatureBytes[ethercrypto.RecoveryIDOffset] == 27 || signatureBytes[ethercrypto.RecoveryIDOffset] == 28 {
      signatureBytes[ethercrypto.RecoveryIDOffset] -= 27
    }

    // Recover public key from signature
    recoveredPublicKey, err := ethercrypto.SigToPub(msgHash, signatureBytes)
    if err != nil {
      return err
    }
    uncompressedPublicKeyBytes := ethercrypto.FromECDSAPub(recoveredPublicKey)

    // Bech-32 address extraction

    // remove prefix byte
    sanitisedPublicKey := uncompressedPublicKeyBytes[1:]

    // Hash with Kekkac256
    addressKekkackHash := kekkackHash(sanitisedPublicKey)

    // Use last 20 bytes
    addressHash := addressKekkackHash[len(addressKekkackHash)-20:]

   // Encode to Bech32
   addr, err := hexToBech32("osmo", addressHash)
    if err != nil {
      panic(err)
    }

  fmt.Println("Address in Bech 32: %v", addr)
}

func hexToBech32(hrp string, addressBytes []byte) (string, error) {
    converted, err := bech32.ConvertBits(addressBytes, 8, 5, true)
    if err != nil {
        return "", fmt.Errorf("encoding bech32 failed: %w", err)
    }

    return bech32.Encode(hrp, converted)
}

func kekkackHash(data []byte) []byte {
    hasher := sha3.NewLegacyKeccak256()
    hasher.Write(data)
    return hasher.Sum(nil)
}