btcsuite / btcutil

Provides bitcoin-specific convenience functions and types
475 stars 405 forks source link

How to validate bech32 signed message? #189

Closed ping-localhost closed 3 years ago

ping-localhost commented 3 years ago

Hello,

I've been trying to validate a signed bech32 message (from Electrum, BIP-0137) but I keep running into walls, since my knowledge related to ECDSA, addresses and signatures is pretty limited.

I was able to extract PublicKey using the code below, but it doesn't match address and signature, err := btcec.ParseSignature(signatureEncoded, btcec.S256()) errors out with malformed signature: no header magic. I have a feeling that it's because the signature isn't provided in the proper format, not sure how to get there though.

If anyone could help me out that would be greatly appreciated. I tried to find a Slack/Mattermost/Gitter channel but haven't found any otherwise I would've asked there.

package main

import (
    "encoding/base64"
    "fmt"

    "github.com/btcsuite/btcd/btcec"
    "github.com/btcsuite/btcd/chaincfg/chainhash"
)

const (
    message           = "Hello"
    address           = "bc1qsdjne3y6ljndzvg9z9qrhje8k7p2m5yas704hn"
    electrumSignature = "H0wOFGpArXjGOdT57eb902p9SsZ4ELAtMLL8hATpeQerCdheTSHf6qP8y+x83nhV30MEXsa9Gji8+Z9OZKdSs2E="
)

func main() {
    signatureEncoded, err := base64.StdEncoding.DecodeString(electrumSignature)
    if err != nil {
        fmt.Println(err)
        return
    }

    pubKey, _, err := btcec.RecoverCompact(btcec.S256(), signatureEncoded, []byte(message))
    if err != nil {
        fmt.Println(err)
        return
    }

    signature, err := btcec.ParseSignature(signatureEncoded, btcec.S256())
    if err != nil {
        fmt.Println(err)
        return
    }

    // Verify the signature for the message using the public key.
    messageHash := chainhash.DoubleHashB([]byte(message))
    verified := signature.Verify(messageHash, pubKey)
    fmt.Println("Signature Verified?", verified)
}
Joukehofman commented 3 years ago

Small script that I hacked together that should not be used in any production environment, but is used to explain the points above:

package main

import (
    "encoding/base64"
    "errors"
    "fmt"
    "math/big"

    "github.com/btcsuite/btcd/btcec"
    "github.com/btcsuite/btcd/chaincfg"
    "github.com/btcsuite/btcd/chaincfg/chainhash"
    "github.com/btcsuite/btcutil"
)

const (
    message           = "Hello"
    address           = "bc1qsdjne3y6ljndzvg9z9qrhje8k7p2m5yas704hn"
    electrumSignature = "H0wOFGpArXjGOdT57eb902p9SsZ4ELAtMLL8hATpeQerCdheTSHf6qP8y+x83nhV30MEXsa9Gji8+Z9OZKdSs2E="
)

func main() {
    addr, err := btcutil.DecodeAddress(address, &chaincfg.MainNetParams)
    if err != nil {
        panic(err)
    }
    //TODO: use compactLen. This works now because the message isn't long
    magicMessage := "\x18Bitcoin Signed Message:\n" + string(len(message)) + message

    messageHash := chainhash.DoubleHashB([]byte(magicMessage))

    signatureEncoded, err := base64.StdEncoding.DecodeString(electrumSignature)
    if err != nil {
        panic(err)
    }

    pubKey, comp, err := btcec.RecoverCompact(btcec.S256(), signatureEncoded, messageHash)
    if err != nil {
        panic(err)

    }
    fmt.Println("Pubkey is compressed?", comp)
    // TODO: switch if not compressed or better if the recovery flag in the signature tells us to do so
    // TODO: add P2WPKH-P2SH checking
    pubkeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
    p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash(pubkeyHash, &chaincfg.MainNetParams)
    if err != nil {
        panic(err)
    }

    signature, err := parseCompact(signatureEncoded, btcec.S256())
    if err != nil {
        panic(err)

    }

    verified := signature.Verify(messageHash, pubKey)
    fmt.Println("Signature Verified?", verified)

    // TODO: check for P2PKH/P2WPKH-P2SH/P2WPKH
    fmt.Println("Addresses are the same?", addr.String() == p2wkhAddr.String())
}

func parseCompact(signature []byte, curve *btcec.KoblitzCurve) (*btcec.Signature, error) {
    bitlen := (curve.BitSize + 7) / 8
    if len(signature) != 1+bitlen*2 {
        return nil, errors.New("invalid compact signature size")
    }

    sig := &btcec.Signature{
        R: new(big.Int).SetBytes(signature[1 : bitlen+1]),
        S: new(big.Int).SetBytes(signature[bitlen+1:]),
    }
    return sig, nil
}
ping-localhost commented 3 years ago

Ah yes, that makes so much more sense @Joukehofman. Most of the things you've in your snippet I have seen, was just unable to connect it all together. Thank you so much for this initial snippet.