diff --git a/.golangci.yml b/.golangci.yml index c31ef8e..952c2ad 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -60,7 +60,6 @@ linters: - gocritic - gocyclo - godot - - godox - gofmt - gofumpt - goheader diff --git a/README.md b/README.md index 320c58f..81a5c72 100644 --- a/README.md +++ b/README.md @@ -28,24 +28,51 @@ For examples, checkout the [example](/.example) folder. ## Support -This library tries to support as many signatures as possible. +This library tries to support as many signatures as possible, as long as they properly follow specifications. + +### Generic / BIP-0137 + +This specification is considered [legacy signing in BIP-322](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#legacy). + +#### Supported -**Current support:** - Any wallet that does signing like Electrum, example: - - Electrum: P2PKH, P2WPKH and P2WPKH-P2SH - - Coinomi: P2PKH, P2WPKH and P2WPKH-P2SH - - Samourai: P2PKH, P2WPKH and P2WPKH-P2SH - - Mycelium: P2PKH, P2WPKH and P2WPKH-P2SH + - Electrum: P2PKH, P2WPKH and P2SH-P2WPKH + - Coinomi: P2PKH, P2WPKH and P2SH-P2WPKH + - Samourai: P2PKH, P2WPKH and P2SH-P2WPKH + - Mycelium: P2PKH, P2WPKH and P2SH-P2WPKH - Any wallet that allows for legacy address signatures (P2PKH), example: - Bitcoin Core - Any wallet that follows [BIP 137](https://github.com/bitcoin/bips/blob/master/bip-0137.mediawiki), example: - - Trezor: P2PKH, P2WPKH and P2WPKH-P2SH + - Trezor: P2PKH, P2WPKH and P2SH-P2WPKH - Taproot (P2TR) - The verification is using the internal key, so only addresses without a tapscript are allowed. -**Currently not supported:** +#### Not supported + - Pay-to-Witness-Script-Hash (P2WSH) -- BIP-322 + +### BIP-322 + +#### Supported + +- [Simple singing](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#simple) + - P2WPKH - Native Segwit + - P2TR - Taproot + +#### Not supported + +- [Simple singing](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#simple) of other types +- [Full signing](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full) +- [Full singing (Proof of Funds)](https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full-proof-of-funds) +- Multisig of any kind + +### UniSat + +The UniSat wallet [used to not follow established standards](https://github.com/BitonicNL/verify-signed-message/issues/3#issuecomment-1597101994) for signing messages when using non-taproot addresses. Specifically, it used to set incorrect recovery flags, resulting in signatures that are seen as invalid by Electrum, Bitcoin Core, Trezor, etc. + +This seems to have been resolved in recent versions of Unisat. Not sure if they resolved it or one of their dependencies resolved it, but in our latest tests it worked as expected. +If you run into issues, make sure you are using the latest version and generate new signatures. ## Development diff --git a/go.mod b/go.mod index ddbfbab..2a09767 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/bitonicnl/verify-signed-message -go 1.18 +go 1.21 require ( github.com/btcsuite/btcd v0.24.0 @@ -17,8 +17,8 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect + golang.org/x/sys v0.20.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cd0b3b8..d571314 100644 --- a/go.sum +++ b/go.sum @@ -79,10 +79,10 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45 golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4= +golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -98,8 +98,8 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/internal/bip322/doc.go b/internal/bip322/doc.go new file mode 100644 index 0000000..92da9c0 --- /dev/null +++ b/internal/bip322/doc.go @@ -0,0 +1,8 @@ +// Package bip322 holds all the relevant tools to actually build and validate signed message via BIP-322. +// +// For more information, refer: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki +// +// These files were heavily inspired by: +// - https://github.com/brc20-devs/brc20-reference-implementation/blob/main/modules/brc20_swap_index/lib/bip322/verify.go +// - https://github.com/babylonchain/babylon/tree/dev/crypto/bip322 +package bip322 diff --git a/internal/bip322/pbst.go b/internal/bip322/pbst.go new file mode 100644 index 0000000..7ca97b6 --- /dev/null +++ b/internal/bip322/pbst.go @@ -0,0 +1,151 @@ +package bip322 + +import ( + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + + "github.com/bitonicnl/verify-signed-message/internal" +) + +// Constants for the toSpend transaction. +const ( + // toSpendVersion contains the transaction version. + toSpendVersion = 0 + // toSpendLockTime contains the transaction lock time. + toSpendLockTime = 0 + // toSpendInputHash contains the dummy input hash. + toSpendInputHash = "0000000000000000000000000000000000000000000000000000000000000000" + // toSpendInputIndex contains the dummy input index. + toSpendInputIndex = 0xFFFFFFFF + // toSpendInputSeq contains the sequence number for the input. + toSpendInputSeq = 0 + // toSpendOutputValue contains the output value (in satoshis). + toSpendOutputValue = 0 +) + +// Constants for the toSign transaction. +const ( + // toSignVersion contains the transaction version. + toSignVersion = 0 + // toSignLockTime contains the transaction lock time. + toSignLockTime = 0 + // toSignInputSeq contains the sequence number for the input. + toSignInputSeq = 0 + // toSignOutputValue contains the output value (in satoshis). + toSignOutputValue = 0 +) + +// BuildToSpendTx builds a toSpend transaction based on the BIP-322 spec. It requires the message that is signed and the address that produced the signature. +// +// For more details, refer: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full +func BuildToSpendTx(msg []byte, address btcutil.Address) (*wire.MsgTx, error) { + // Create a new transaction + psbt := wire.NewMsgTx(toSpendVersion) + psbt.LockTime = toSpendLockTime + + // Create an outpoint for the input + inputHash, err := chainhash.NewHashFromStr(toSpendInputHash) + if err != nil { + // This error indicates a programming error since the input hash is predefined + panic(err) + } + outPoint := wire.NewOutPoint(inputHash, toSpendInputIndex) + + // Generate the signature script for the input + script, err := toSpendSignatureScript(msg) + if err != nil { + return nil, err + } + + // Create the input using the outpoint and signature script + input := wire.NewTxIn(outPoint, script, nil) + input.Sequence = toSpendInputSeq + + // Create the output paying to the provided address + pkScript, err := txscript.PayToAddrScript(address) + if err != nil { + return nil, err + } + + // Create the output using the pay-to-address script + output := wire.NewTxOut(toSpendOutputValue, pkScript) + + // Add the input and output to the transaction + psbt.AddTxIn(input) + psbt.AddTxOut(output) + + return psbt, nil +} + +// BuildToSignTx builds a toSign transaction based on the BIP-322 spec. // It requires the toSpend transaction that it spends. +// +// For more details, refer: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full +func BuildToSignTx(toSpend *wire.MsgTx) *wire.MsgTx { + // Create a new transaction + toSign := wire.NewMsgTx(toSignVersion) + toSign.LockTime = toSignLockTime + + // Specify the input outpoint + // As the input is from the toSpend transaction, the index is 0 + inputHash := toSpend.TxHash() + outPoint := wire.NewOutPoint(&inputHash, 0) + + // Create the input using the out point + input := wire.NewTxIn(outPoint, nil, nil) + input.Sequence = toSignInputSeq + + // Create the output with an unspendable script + output := wire.NewTxOut(toSignOutputValue, buildSignPkScript()) + + // Add the input and output to the transaction + toSign.AddTxIn(input) + toSign.AddTxOut(output) + + return toSign +} + +// toSpendSignatureScript creates the signature script for the input of the toSpend transaction. It follows the BIP-322 specification. +// +// For more details, refer: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full +func toSpendSignatureScript(msg []byte) ([]byte, error) { + // Create a new script builder + builder := txscript.NewScriptBuilder() + + // Add OP_0 to initialize the witness stack + builder.AddOp(txscript.OP_0) + + // Create the magic message as specified in BIP-322 + data := internal.CreateMagicMessageBIP322(msg) + builder.AddData(data[:]) + + // Generate the script + script, err := builder.Script() + if err != nil { + // Since this is based on the incoming message, this could happen + return nil, err + } + + return script, nil +} + +// buildSignPkScript creates the public key script for the output of the toSign transaction. It follows the BIP-322 specification. +// +// For more details, refer: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full +func buildSignPkScript() []byte { + // Create a new script builder + builder := txscript.NewScriptBuilder() + + // Add OP_RETURN opcode to mark the output as unspendable + builder.AddOp(txscript.OP_RETURN) + + // Generate the script + script, err := builder.Script() + if err != nil { + // Since we are constructing the script, this error should not occur in practice + panic(err) + } + + return script +} diff --git a/internal/bip322/pbst_test.go b/internal/bip322/pbst_test.go new file mode 100644 index 0000000..eb43b25 --- /dev/null +++ b/internal/bip322/pbst_test.go @@ -0,0 +1,32 @@ +package bip322_test + +import ( + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/stretchr/testify/require" + + "github.com/bitonicnl/verify-signed-message/internal/bip322" +) + +// Taken from https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#transaction-hashes +func TestGetToSignTx(t *testing.T) { + t.Parallel() + + testAddr := "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l" + testAddrDecoded, err := btcutil.DecodeAddress(testAddr, &chaincfg.TestNet3Params) + require.NoError(t, err) + + toSpendTx, err := bip322.BuildToSpendTx([]byte{}, testAddrDecoded) + require.NoError(t, err) + require.Equal(t, "c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7", toSpendTx.TxHash().String()) + toSignTx := bip322.BuildToSignTx(toSpendTx) + require.Equal(t, "1e9654e951a5ba44c8604c4de6c67fd78a27e81dcadcfe1edf638ba3aaebaed6", toSignTx.TxHash().String()) + + toSpendTx, err = bip322.BuildToSpendTx([]byte("Hello World"), testAddrDecoded) + require.NoError(t, err) + require.Equal(t, "b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b", toSpendTx.TxHash().String()) + toSignTx = bip322.BuildToSignTx(toSpendTx) + require.Equal(t, "88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf", toSignTx.TxHash().String()) +} diff --git a/internal/bip322/verify.go b/internal/bip322/verify.go new file mode 100644 index 0000000..e4bf863 --- /dev/null +++ b/internal/bip322/verify.go @@ -0,0 +1,71 @@ +package bip322 + +import ( + "errors" + "fmt" + "reflect" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/txscript" +) + +// TODO: Check if we can implement more by referencing https://github.com/ACken2/bip322-js/blob/main/src/Verifier.ts#L23 +// Their implementation supports *btcutil.AddressScriptHash (but no multisig, yet). +func Verify(address btcutil.Address, message string, signatureDecoded []byte) (bool, error) { + // Ensure we support the address + if !IsSupported(address) { + return false, fmt.Errorf("unsupported address type '%s'", reflect.TypeOf(address)) + } + + // Draft corresponding toSpend and toSign transaction using the message and script pubkey + toSpend, err := BuildToSpendTx([]byte(message), address) + if err != nil { + return false, fmt.Errorf("could not build spending transaction: %w", err) + } + + witness, err := SimpleSigToWitness(signatureDecoded) + if err != nil { + return false, fmt.Errorf("error converting signature into witness: %w", err) + } + + toSign := BuildToSignTx(toSpend) + toSign.TxIn[0].Witness = witness + + // Validate toSign transaction + if len(toSign.TxIn) != 1 || len(toSign.TxOut) != 1 { + return false, errors.New("invalid toSign transaction format") + } + + // From the rules here: + // https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#verification-process + // We only need to perform verification of whether toSign spends toSpend properly + // given that the signature is a simple one, and we construct both toSpend and toSign + inputFetcher := txscript.NewCannedPrevOutputFetcher(toSpend.TxOut[0].PkScript, 0) + sigHashes := txscript.NewTxSigHashes(toSign, inputFetcher) + vm, err := txscript.NewEngine(toSpend.TxOut[0].PkScript, toSign, 0, txscript.StandardVerifyFlags, txscript.NewSigCache(0), sigHashes, toSpend.TxOut[0].Value, inputFetcher) + if err != nil { + return false, fmt.Errorf("could not create new engine: %w", err) + } + + // Execute the script + err = vm.Execute() + if err != nil { + return false, fmt.Errorf("script execution failed: %w", err) + } + + // Verification successful + return true, nil +} + +func IsSupported(address btcutil.Address) bool { + switch address.(type) { + // P2WPKH - Native Segwit + case *btcutil.AddressWitnessPubKeyHash: + return true + // P2TR - Taproot + case *btcutil.AddressTaproot: + return true + default: + return false + } +} diff --git a/internal/bip322/verify_test.go b/internal/bip322/verify_test.go new file mode 100644 index 0000000..77542fe --- /dev/null +++ b/internal/bip322/verify_test.go @@ -0,0 +1,189 @@ +package bip322_test + +import ( + "encoding/base64" + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/stretchr/testify/suite" + + "github.com/bitonicnl/verify-signed-message/internal/bip322" + verifier "github.com/bitonicnl/verify-signed-message/pkg" +) + +type VerifyTestSuite struct { + suite.Suite +} + +func TestVerifyTestSuite(t *testing.T) { + // Run everything in parallel + t.Parallel() + + suite.Run(t, new(VerifyTestSuite)) +} + +func (s *VerifyTestSuite) TestVerifyIncorrect() { + tests := map[string]struct { + signedMessage verifier.SignedMessage + expectedError string + }{ + // Taken from https://github.com/luke-jr/bitcoin/blob/9ab7b8ada61a5f558c92c3eb9fd3cd3625d8cc09/src/test/util_tests.cpp#L1774 + "native segwit - wrong address": { + signedMessage: verifier.SignedMessage{ + Address: "bc1qkecg9ly2xwxqgdy9egpuy87qc9x26smpts562s", + Message: "", + Signature: "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", + }, + expectedError: "script execution failed: OP_EQUALVERIFY failed", + }, + // Taken from https://github.com/luke-jr/bitcoin/blob/9ab7b8ada61a5f558c92c3eb9fd3cd3625d8cc09/src/test/util_tests.cpp#L1790 + "native segwit - malformed address": { + signedMessage: verifier.SignedMessage{ + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "", + Signature: "AkcwRAIgClVQ8S9yX1h8YThlGElD9lOrQbOwbFDjkYb0ebfiq+oCIDHgb/X9WNalNNtqTXb465ufbv9JuLxcJf8qi7DP6yOXASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", + }, + expectedError: "script execution failed: signature not empty on failed checksig", + }, + // Taken from https://github.com/ACken2/bip322-js/blob/159456f44f31f0b38097b957bbe75c0eae4971bf/test/Verifier.test.ts#L114 + "segwit": { + signedMessage: verifier.SignedMessage{ + Address: "3HSVzEhCFuH9Z3wvoWTexy7BMVVp3PjS6f", + Message: "Hello World", + Signature: "AkgwRQIhAMd2wZSY3x0V9Kr/NClochoTXcgDaGl3OObOR17yx3QQAiBVWxqNSS+CKen7bmJTG6YfJjsggQ4Fa2RHKgBKrdQQ+gEhAxa5UDdQCHSQHfKQv14ybcYm1C9y6b12xAuukWzSnS+w\n", + }, + expectedError: "unsupported address type '*btcutil.AddressScriptHash'", + }, + // Taken from https://github.com/ACken2/bip322-js/blob/159456f44f31f0b38097b957bbe75c0eae4971bf/test/Verifier.test.ts#L302 + "taproot - script-spend": { + signedMessage: verifier.SignedMessage{ + Address: "bc1p3r88nsysd8sv555nur4h85wdupa5z0xpcgcdjxy5up30re8gcneswrkwkv", + Message: "Hello World - This should fail", + Signature: "A4AxODdkNTJkNGVkNDQ2OThlY2M5NjJlZDc0ZDdmODIyODIwNDc1YTc1NjdjMTViYmFkOGY5MWNlOTZkMGYxMzJkMmQxM2U0MzA3OWFlNzAwMTE5YzkxYTQ2MjA4Yzk5NWUzYTE4YjUzNjYzNjhkZDA0NDUwYzNmZjU2NTIyMWQyY+AyMDVkZTgxNTRlNzBkNmFmNTI5MDZhNGM0ZDc4OThiMDE4MGRlNWRiOGI3Y2Q0NGNiZDI3Y2RkZmY3NzUxY2ViYzdhYzAwNjMwMzZmNzI2NDAxMDExODc0NjU3ODc0MmY3MDZjNjE2OTZlM2I2MzY4NjE3MjczNjU3NDNkNzU3NDY2MmQzODAwMmE3YjIyNzAyMjNhMjI3MzZlNzMyMjJjMjI2ZjcwMjIzYTIyNzI2NTY3MjIyYzIyNmU2MTZkNjUyMjNhMjIzNjMzMzEzMjM4MmU3MzYxNzQ3MzIyN2Q2OEJjMDVkZTgxNTRlNzBkNmFmNTI5MDZhNGM0ZDc4OThiMDE4MGRlNWRiOGI3Y2Q0NGNiZDI3Y2RkZmY3NzUxY2ViYzc=", + }, + expectedError: "script execution failed: control block proof is not a multiple of 32: 33", + }, + // Taken from https://github.com/luke-jr/bitcoin/blob/9ab7b8ada61a5f558c92c3eb9fd3cd3625d8cc09/src/test/util_tests.cpp#L1765 + "taproot - wrong message": { + signedMessage: verifier.SignedMessage{ + Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", + Message: "Hello World - This should fail", + Signature: "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==", + }, + expectedError: "script execution failed: ", + }, + // Taken from https://github.com/luke-jr/bitcoin/blob/9ab7b8ada61a5f558c92c3eb9fd3cd3625d8cc09/src/test/util_tests.cpp#L1730 + "p2sh - 2-of-3 multisig": { + signedMessage: verifier.SignedMessage{ + Address: "3LnYoUkFrhyYP3V7rq3mhpwALz1XbCY9Uq", + Message: "This will be a p2sh 2-of-3 multisig BIP 322 signed message", + Signature: "AAAAAAHNcfHaNfl8f/+ZC2gTr8aF+0KgppYjKM94egaNm/u1ZAAAAAD8AEcwRAIhAJ6hdj61vLDP+aFa30qUZQmrbBfE0kiOObYvt5nqPSxsAh9IrOKFwflfPRUcQ/5e0REkdFHVP2GGdUsMgDet+sNlAUcwRAIgH3eW/VyFDoXvCasd8qxgwj5NDVo0weXvM6qyGXLCR5YCIEwjbEV6fS6RWP6QsKOcMwvlGr1/SgdCC6pW4eH87/YgAUxpUiECKJfGy28imLcuAeNBLHCNv3NRP5jnJwFDNRXCYNY/vJ4hAv1RQtaZs7+vKqQeWl2rb/jd/gMxkEjUnjZdDGPDZkMLIQL65cH2X5O7LujjTLDL2l8Pxy0Y2UUR99u1qCfjdz7dklOuAAAAAAEAAAAAAAAAAAFqAAAAAA==", + }, + expectedError: "unsupported address type '*btcutil.AddressScriptHash'", + }, + // Taken from https://github.com/luke-jr/bitcoin/blob/9ab7b8ada61a5f558c92c3eb9fd3cd3625d8cc09/src/test/util_tests.cpp#L1743 + "p2wsh - 3-of-3 multisig": { + signedMessage: verifier.SignedMessage{ + Address: "bc1qlqtuzpmazp2xmcutlwv0qvggdvem8vahkc333usey4gskug8nutsz53msw", + Message: "This will be a p2wsh 3-of-3 multisig BIP 322 signed message", + Signature: "BQBIMEUCIQDQoXvGKLH58exuujBOta+7+GN7vi0lKwiQxzBpuNuXuAIgIE0XYQlFDOfxbegGYYzlf+tqegleAKE6SXYIa1U+uCcBRzBEAiATegywVl6GWrG9jJuPpNwtgHKyVYCX2yfuSSDRFATAaQIgTLlU6reLQsSIrQSF21z3PtUO2yAUseUWGZqRUIE7VKoBSDBFAiEAgxtpidsU0Z4u/+5RB9cyeQtoCW5NcreLJmWXZ8kXCZMCIBR1sXoEinhZE4CF9P9STGIcMvCuZjY6F5F0XTVLj9SjAWlTIQP3dyWvTZjUENWJowMWBsQrrXCUs20Gu5YF79CG5Ga0XSEDwqI5GVBOuFkFzQOGH5eTExSAj2Z/LDV/hbcvAPQdlJMhA17FuuJd+4wGuj+ZbVxEsFapTKAOwyhfw9qpch52JKxbU64=", + }, + expectedError: "unsupported address type '*btcutil.AddressWitnessScriptHash'", + }, + "Pay-to-Witness-Script-Hash - P2WSH": { + signedMessage: verifier.SignedMessage{ + Address: "bc1qeklep85ntjz4605drds6aww9u0qr46qzrv5xswd35uhjuj8ahfcqgf6hak", + Message: "doesn't matter", + Signature: "ZG9lc24ndCBtYXR0ZXI=", + }, + expectedError: "unsupported address type '*btcutil.AddressWitnessScriptHash'", + }, + } + + for name, tt := range tests { + s.Run(name, func() { + // Decode the address + address, err := btcutil.DecodeAddress(tt.signedMessage.Address, &chaincfg.MainNetParams) + s.Require().NoError(err) + + // Decode the signature + signatureDecoded, err := base64.StdEncoding.DecodeString(tt.signedMessage.Signature) + s.Require().NoError(err) + + valid, err := bip322.Verify(address, tt.signedMessage.Message, signatureDecoded) + s.Require().EqualError(err, tt.expectedError) + s.False(valid) + }) + } +} + +func (s *VerifyTestSuite) TestVerify() { + tests := map[string]verifier.SignedMessage{ + // BIP-322 test vector #0 - https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#user-content-Test_vectors + "test vector #0": { + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "Hello World", + Signature: "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", + }, + // BIP-322 test vector #1 - https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#user-content-Test_vectors + "test vector #1": { + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "", + Signature: "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", + }, + // bip-322 signature created using buidl-python library with same parameters as https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#user-content-Test_vectors + "buidl-python - test vector #0": { + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "Hello World", + Signature: "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy", + }, + // Generated via the Leather Wallet, using the same words as for unisat + "leather - segwit native": { + Address: "bc1qvhnxd953tzt4kcqpcgk83wu2r9shf59q2t4egu", + Message: "hello", + Signature: "AkgwRQIhAMPtK3P+dVOTFe5w9Rw2IJzjMjAXOXQUaBptg3QcT64JAiAX6TxbLPTetNJA7gKoARU/WH7Owm4YBS7ALeN+2LcBeQEhA59DAKSL/e9Zj9BEfm4DyBlGTAH9/8cYInHmMqbjz8EX", + }, + // Generated via the Leather Wallet, using the same words as for unisat + "leather - taproot": { + Address: "bc1pgc9k3vdmr9aecmwj09qg5qv550qyyrydufyfmxrsvk5474rxenuqrq4lcz", + Message: "hello", + Signature: "AUBuPt7wX3zcAaMs7F/oGXPROspWWIvBh/GqjTQ6uPq8sUPxSIqGGaz8z4yuEoYRzXwaAeXBucxjlygiR02zvX2L", + }, + // Single key taproot bip-322 signature (created with the buidl-python library) + // Taken from: https://github.com/luke-jr/bitcoin/blob/9ab7b8ada61a5f558c92c3eb9fd3cd3625d8cc09/src/test/util_tests.cpp#L1754 + "buidl-python - taproot": { + Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", + Message: "Hello World", + Signature: "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==", + }, + // Single key taproot bip-322 signature (created with the sparrow) + "sparrow - taproot": { + Address: "bc1pqqeyhah6g75dwr942xv40h255q4nshqw4k8ylyhe7plej2eg3mnqz9w4np", + Message: "Taproot, lets go!", + Signature: "AUE8tKBiwiq64JYkSbf+4byheZlmDB5xyasRJ+ujM9/h/BfHFsd4jovtmmEfSsEZTBzoOP9m7We92UEbhqb4sBf4AQ==", + }, + // Single key taproot bip-322 signature (created by nullish.org) + "nullish.org - taproot": { + Address: "bc1pkr9m9rcspdyzhtf7g2pkc2l8ww7yp0prckkvg252edk7pvusx5ts3n5e0x", + Message: "nullish.org", + Signature: "AUHyxHye4t2wc3zE/jj+S9itMJh1+XrqR7aaHtkoKsy/d49gzAJnstbZgdMYh6Ywn+g8tG9U9oqrMNqlVdM8I8R9AQ==", + }, + } + + for name, tt := range tests { + s.Run(name, func() { + // Decode the address + address, err := btcutil.DecodeAddress(tt.Address, &chaincfg.MainNetParams) + s.Require().NoError(err) + + // Decode the signature + signatureDecoded, err := base64.StdEncoding.DecodeString(tt.Signature) + s.Require().NoError(err) + + valid, err := bip322.Verify(address, tt.Message, signatureDecoded) + s.Require().NoError(err) + s.True(valid) + }) + } +} diff --git a/internal/bip322/witness.go b/internal/bip322/witness.go new file mode 100644 index 0000000..24f3d1d --- /dev/null +++ b/internal/bip322/witness.go @@ -0,0 +1,91 @@ +package bip322 + +import ( + "bytes" + "fmt" + "io" + + "github.com/btcsuite/btcd/wire" +) + +// This file is adapted from the btcd package (v0.24.0) message transaction (msgtx) implementation and babylon package (v0.8.5) bip322 implementation. +// +// Original source: +// - https://github.com/btcsuite/btcd/blob/v0.24.0/wire/msgtx.go#L559-L590 +// - https://github.com/babylonchain/babylon/blob/v0.8.5/crypto/bip322/witness.go + +// Constants related to witness data handling. +const ( + // maxWitnessItemsPerInput is the maximum number of witness items to be read for the witness data for a single TxIn. + // This value is derived to ensure that the transaction weight cannot exceed the consensus limit. + maxWitnessItemsPerInput = 4_000_000 + + // maxWitnessItemSize is the maximum allowed size for an item within an input's witness data. + // This value is bounded by the largest possible block size post-SegWit v1 (Taproot). + maxWitnessItemSize = 4_000_000 +) + +// SimpleSigToWitness converts a simple signature into a witness stack. +// As per the BIP-322 spec, a simple signature consists of a witness stack, consensus encoded as a vector of vectors of bytes. +// For more details, refer: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#simple +// +// The encoding of the witness stack is based on the Leather wallet implementation. +// For details, refer: https://github.com/leather-wallet/extension/blob/dev/src/shared/crypto/bitcoin/bip322/bip322-utils.ts#L58 +// +// The signature is encoded as follows: +// - 1st byte: Elements of the witness stack that are serialized +// - For each element of the stack +// - The first byte specifies how many bytes it contains +// - The rest are the bytes of the element +func SimpleSigToWitness(sig []byte) ([][]byte, error) { + // Create a buffer from the input signature. + buf := bytes.NewBuffer(sig) + + // Read the varint encoding the number of stack items. + witCount, err := wire.ReadVarInt(buf, 0) + if err != nil { + return nil, err + } + + // Ensure that the number of stack items is within the maximum allowed limit. + if witCount > maxWitnessItemsPerInput { + return nil, fmt.Errorf("too many witness items to fit into max message size [count %d, max %d]", witCount, maxWitnessItemsPerInput) + } + + // Read each stack item from the buffer. + witnessStack := make([][]byte, witCount) + for j := uint64(0); j < witCount; j++ { + witnessStack[j], err = readScript(buf, 0, maxWitnessItemSize, "script witness item") + if err != nil { + return nil, err + } + } + + return witnessStack, nil +} + +// readScript reads a variable length byte array that represents a transaction script. +// It is encoded as a varInt containing the length of the array followed by the bytes themselves. +// This function provides protection against memory exhaustion attacks and malformed messages. +// +// For more information, refer: https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer +func readScript(r io.Reader, pver, maxAllowed uint32, fieldName string) ([]byte, error) { + count, err := wire.ReadVarInt(r, pver) + if err != nil { + return nil, err + } + + // Ensure that the byte array is within the maximum allowed size to prevent memory exhaustion attacks. + if count > uint64(maxAllowed) { + return nil, fmt.Errorf("%s is larger than the max allowed size [count %d, max %d]", fieldName, count, maxAllowed) + } + + // Read the byte array. + b := make([]byte, count) + _, err = io.ReadFull(r, b) + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/internal/bip322/witness_test.go b/internal/bip322/witness_test.go new file mode 100644 index 0000000..d1b9809 --- /dev/null +++ b/internal/bip322/witness_test.go @@ -0,0 +1,29 @@ +package bip322_test + +import ( + "encoding/base64" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/bitonicnl/verify-signed-message/internal/bip322" +) + +func TestSimpleSigToWitness(t *testing.T) { + t.Parallel() + + signatureEncoded := "AkcwRAIgbAFRpM0rhdBlXr7qe5eEf3XgSeausCm2XTmZVxSYpcsCIDcbR87wF9DTrvdw1czYEEzOjso52dOSaw8VrC4GgzFRASECO5NGNFlPClJnTHNDW94h7pPL5D7xbl6FBNTrGaYpYcA=" + emptyBytesSig, err := base64.StdEncoding.DecodeString(signatureEncoded) + require.NoError(t, err) + + witness, err := bip322.SimpleSigToWitness(emptyBytesSig) + require.NoError(t, err) + require.Len(t, witness, 2) + + firstWitness := hex.EncodeToString(witness[0]) + require.Equal(t, "304402206c0151a4cd2b85d0655ebeea7b97847f75e049e6aeb029b65d3999571498a5cb0220371b47cef017d0d3aef770d5ccd8104cce8eca39d9d3926b0f15ac2e0683315101", firstWitness) + + secondWitness := hex.EncodeToString(witness[1]) + require.Equal(t, "023b934634594f0a52674c73435bde21ee93cbe43ef16e5e8504d4eb19a62961c0", secondWitness) +} diff --git a/internal/generic/doc.go b/internal/generic/doc.go new file mode 100644 index 0000000..03e5bc0 --- /dev/null +++ b/internal/generic/doc.go @@ -0,0 +1,4 @@ +// Package generic holds all the relevant tools to actually build and validate signed message made by older signed message implementations. +// +// For more information, refer: https://en.bitcoin.it/wiki/Message_signing#Message_verification_method +package generic diff --git a/internal/generic/flags/recovery_flags.go b/internal/generic/flags/recovery_flags.go new file mode 100644 index 0000000..fe6bf4b --- /dev/null +++ b/internal/generic/flags/recovery_flags.go @@ -0,0 +1,53 @@ +package flags + +// All returns every possible recovery flag, taken from https://github.com/btclib-org/btclib/blob/v2022.7.20/btclib/ecc/bms.py#L83 +func All() []int { + return []int{27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42} +} + +// Compressed returns all compressed recovery flags. +func Compressed() []int { + return []int{31, 32, 33, 34} +} + +// ElectrumP2WPKH returns all P2WPKH recovery flags related to Electrum. +func ElectrumP2WPKH() []int { + return []int{31, 32, 33, 34} +} + +// ElectrumP2SHAndP2WPKH returns all P2SH-P2WPKH recovery flags related to Electrum. +func ElectrumP2SHAndP2WPKH() []int { + return []int{31, 32, 33, 34} +} + +// GetKeyID returns the Key ID for a specified recovery flag +// Taken from https://github.com/btclib-org/btclib/blob/v2023.7.12/btclib/ecc/bms.py#L302 +func GetKeyID(recoveryFlag int) int { + return (recoveryFlag - 27) & 0b11 +} + +// ShouldBeCompressed returns if a recovery flag signals a compressed key +// Taken from https://github.com/btclib-org/btclib/blob/v2023.7.12/btclib/ecc/bms.py#L305 +func ShouldBeCompressed(recoveryFlag int) bool { + return recoveryFlag > 30 +} + +// Trezor returns all recovery flags related to Trezor. +func Trezor() []int { + return append(TrezorP2SHAndP2WPKH(), TrezorP2WPKH()...) +} + +// TrezorP2WPKH returns all P2WPKH recovery flags related to Trezor. +func TrezorP2WPKH() []int { + return []int{39, 40, 41, 42} +} + +// TrezorP2SHAndP2WPKH returns all P2SH-P2WPKH recovery flags related to Trezor. +func TrezorP2SHAndP2WPKH() []int { + return []int{35, 36, 37, 38} +} + +// Uncompressed returns all uncompressed recovery flags. +func Uncompressed() []int { + return []int{27, 28, 29, 30} +} diff --git a/internal/generic/flags/recovery_flags_test.go b/internal/generic/flags/recovery_flags_test.go new file mode 100644 index 0000000..15334ca --- /dev/null +++ b/internal/generic/flags/recovery_flags_test.go @@ -0,0 +1,95 @@ +package flags_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/bitonicnl/verify-signed-message/internal/generic/flags" +) + +type RecoveryFlagTestSuite struct { + suite.Suite +} + +func TestRecoveryFlagTestSuite(t *testing.T) { + // Run everything in parallel + t.Parallel() + + suite.Run(t, new(RecoveryFlagTestSuite)) +} + +func (s *RecoveryFlagTestSuite) TestAll() { + s.Equal([]int{27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42}, flags.All()) +} + +func (s *RecoveryFlagTestSuite) TestCompressed() { + s.Equal([]int{31, 32, 33, 34}, flags.Compressed()) +} + +func (s *RecoveryFlagTestSuite) TestElectrumP2WPKH() { + s.Equal([]int{31, 32, 33, 34}, flags.ElectrumP2WPKH()) +} + +func (s *RecoveryFlagTestSuite) TestElectrumP2SHAndP2WPKH() { + s.Equal([]int{31, 32, 33, 34}, flags.ElectrumP2SHAndP2WPKH()) +} + +func (s *RecoveryFlagTestSuite) TestGetKeyID() { + tests := []struct { + name string + recoveryFlag int + expectedKeyID int + }{ + {name: "30", recoveryFlag: 30, expectedKeyID: 3}, + {name: "31", recoveryFlag: 31, expectedKeyID: 0}, + {name: "32", recoveryFlag: 32, expectedKeyID: 1}, + {name: "33", recoveryFlag: 33, expectedKeyID: 2}, + {name: "34", recoveryFlag: 34, expectedKeyID: 3}, + {name: "35", recoveryFlag: 35, expectedKeyID: 0}, + } + + for _, tt := range tests { + s.T().Run(tt.name, func(t *testing.T) { + if got := flags.GetKeyID(tt.recoveryFlag); got != tt.expectedKeyID { + t.Errorf("GetKeyID() = %v, want %v", got, tt.expectedKeyID) + } + }) + } +} + +func (s *RecoveryFlagTestSuite) TestShouldBeCompressed() { + tests := []struct { + name string + recoveryFlag int + expected bool + }{ + {name: "30", recoveryFlag: 30, expected: false}, + {name: "31", recoveryFlag: 31, expected: true}, + {name: "32", recoveryFlag: 32, expected: true}, + } + + for _, tt := range tests { + s.T().Run(tt.name, func(t *testing.T) { + if got := flags.ShouldBeCompressed(tt.recoveryFlag); got != tt.expected { + t.Errorf("ShouldBeCompressed() = %v, want %v", got, tt.expected) + } + }) + } +} + +func (s *RecoveryFlagTestSuite) TestTrezor() { + s.Equal([]int{35, 36, 37, 38, 39, 40, 41, 42}, flags.Trezor()) +} + +func (s *RecoveryFlagTestSuite) TestTrezorP2WPKH() { + s.Equal([]int{39, 40, 41, 42}, flags.TrezorP2WPKH()) +} + +func (s *RecoveryFlagTestSuite) TestTrezorP2SHAndP2WPKH() { + s.Equal([]int{35, 36, 37, 38}, flags.TrezorP2SHAndP2WPKH()) +} + +func (s *RecoveryFlagTestSuite) TestUncompressed() { + s.Equal([]int{27, 28, 29, 30}, flags.Uncompressed()) +} diff --git a/internal/generic/hash.go b/internal/generic/hash.go new file mode 100644 index 0000000..b06c5cc --- /dev/null +++ b/internal/generic/hash.go @@ -0,0 +1,18 @@ +package generic + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/samber/lo" + + "github.com/bitonicnl/verify-signed-message/internal/generic/flags" +) + +// GeneratePublicKeyHash returns the public key hash, either compressed or uncompressed, depending on the recovery flag. +func GeneratePublicKeyHash(recoveryFlag int, publicKey *btcec.PublicKey) []byte { + if lo.Contains[int](flags.Uncompressed(), recoveryFlag) { + return btcutil.Hash160(publicKey.SerializeUncompressed()) + } + + return btcutil.Hash160(publicKey.SerializeCompressed()) +} diff --git a/internal/generic/hash_test.go b/internal/generic/hash_test.go new file mode 100644 index 0000000..0047642 --- /dev/null +++ b/internal/generic/hash_test.go @@ -0,0 +1,63 @@ +package generic_test + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/require" + + "github.com/bitonicnl/verify-signed-message/internal/generic" +) + +func TestGeneratePublicKeyHashCompressed(t *testing.T) { + t.Parallel() + + // uint8 representation of `H/iew/NhHV9V9MdUEn/LFOftaTy1ivGPKPKyMlr8OSokNC755fAxpSThNRivwTNsyY9vPUDTRYBPc2cmGd5d4y4=`. + signatureEncoded := []uint8{ + 31, 248, 158, 195, 243, 97, 29, 95, 85, 244, 199, 84, 18, 127, 203, 20, 231, 237, 105, 60, 181, + 138, 241, 143, 40, 242, 178, 50, 90, 252, 57, 42, 36, 52, 46, 249, 229, 240, 49, 165, 36, 225, 53, + 24, 175, 193, 51, 108, 201, 143, 111, 61, 64, 211, 69, 128, 79, 115, 103, 38, 25, 222, 93, 227, 46, + } + + // Grab the recovery flag from the signature + recoveryFlag := int(signatureEncoded[0]) + + // uint8 representation of the public key for `14dD6ygPi5WXdwwBTt1FBZK3aD8uDem1FY`. + publicKeyEncoded := []uint8{ + 3, 77, 160, 6, 249, 88, 190, 186, 120, 236, 84, 68, 61, 244, 163, 245, 34, 55, 37, 63, + 122, 232, 203, 219, 23, 220, 207, 63, 234, 165, 127, 49, 38, + } + + publicKey, err := btcec.ParsePubKey(publicKeyEncoded) + require.NoError(t, err) + + expected := []byte{0x27, 0xc1, 0x74, 0x81, 0x4a, 0x24, 0x4a, 0x65, 0xac, 0xeb, 0xd3, 0xd, 0x74, 0xfa, 0x8d, 0x72, 0x37, 0x98, 0x47, 0x29} + require.Equal(t, expected, generic.GeneratePublicKeyHash(recoveryFlag, publicKey)) +} + +func TestGeneratePublicKeyHash(t *testing.T) { + t.Parallel() + + // uint8 representation of `G/iew/NhHV9V9MdUEn/LFOftaTy1ivGPKPKyMlr8OSokNC755fAxpSThNRivwTNsyY9vPUDTRYBPc2cmGd5d4y4=`. + signatureEncoded := []uint8{ + 27, 248, 158, 195, 243, 97, 29, 95, 85, 244, 199, 84, 18, 127, 203, 20, 231, 237, 105, 60, 181, 138, 241, 143, + 40, 242, 178, 50, 90, 252, 57, 42, 36, 52, 46, 249, 229, 240, 49, 165, 36, 225, 53, 24, 175, 193, 51, + 108, 201, 143, 111, 61, 64, 211, 69, 128, 79, 115, 103, 38, 25, 222, 93, 227, 46, + } + + // Grab the recovery flag from the signature + recoveryFlag := int(signatureEncoded[0]) + + // uint8 representation of the public key for `1HUBHMij46Hae75JPdWjeZ5Q7KaL7EFRSD`. + publicKeyEncoded := []uint8{ + 4, 77, 160, 6, 249, 88, 190, 186, 120, 236, 84, 68, 61, 244, 163, 245, 34, 55, 37, 63, 122, 232, 203, 219, 23, 220, 207, 63, 234, + 165, 127, 49, 38, 218, 10, 9, 9, 241, 25, 152, 19, 12, 45, 14, 134, 164, 133, 244, 231, 158, 228, 102, 161, 131, 164, 118, 196, + 50, 198, 135, 88, 171, 158, 99, 11, + } + + publicKey, err := btcec.ParsePubKey(publicKeyEncoded) + require.NoError(t, err) + + expected := []byte{0xb4, 0xa5, 0xd3, 0x96, 0x4, 0x71, 0x56, 0x8c, 0x38, 0x83, 0x4, 0x6e, 0xec, 0x3b, 0x41, 0xb4, 0x95, 0x3d, 0x61, 0xa1} + require.Equal(t, expected, generic.GeneratePublicKeyHash(recoveryFlag, publicKey)) +} diff --git a/internal/generic/signature/signature.go b/internal/generic/signature/signature.go new file mode 100644 index 0000000..0d98f5f --- /dev/null +++ b/internal/generic/signature/signature.go @@ -0,0 +1,87 @@ +package signature + +import ( + "errors" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" +) + +// Values taken from `ecdsa`. +const ( + // compactSigSize is the size of a compact signature. It consists of a + // compact signature recovery code byte followed by the R and S components + // serialized as 32-byte big-endian values. 1+32*2 = 65. + // for the R and S components. 1+32+32=65. + compactSigSize = 65 + + // compactSigMagicOffset is a value used when creating the compact signature + // recovery code inherited from Bitcoin and has no meaning, but has been + // retained for compatibility. For historical purposes, it was originally + // picked to avoid a binary representation that would allow compact + // signatures to be mistaken for other components. + compactSigMagicOffset = 27 + + // compactSigCompPubKey is a value used when creating the compact signature + // recovery code to indicate the original public key was compressed. + compactSigCompPubKey = 4 +) + +// ParseCompact attempts to recover the ecdsa.Signature from the provided +// compact signature. The logic for this was taken from `ecdsa.RecoverCompact` +// as it is not exposed publicly. +func ParseCompact(signature []byte) (*ecdsa.Signature, error) { + // A compact signature consists of a recovery byte followed by the R and + // S components serialized as 32-byte big-endian values. + if len(signature) != compactSigSize { + return nil, errors.New("invalid compact signature size") + } + + // Parse and validate the compact signature recovery code. + const ( + minValidCode = compactSigMagicOffset + maxValidCode = compactSigMagicOffset + compactSigCompPubKey + 3 + ) + if signature[0] < minValidCode || signature[0] > maxValidCode { + return nil, errors.New("invalid compact signature recovery code") + } + + // Parse and validate the R and S signature components. + // + // Fail if r and s are not in [1, N-1]. + var r, s btcec.ModNScalar + if overflow := r.SetByteSlice(signature[1:33]); overflow { + return nil, errors.New("signature R is >= curve order") + } + if r.IsZero() { + return nil, errors.New("signature R is 0") + } + if overflow := s.SetByteSlice(signature[33:]); overflow { + return nil, errors.New("signature S is >= curve order") + } + if s.IsZero() { + return nil, errors.New("signature S is 0") + } + + return ecdsa.NewSignature(&r, &s), nil +} + +// Verify ensures that the signature for the message hash is valid for the public key given. +func Verify(signatureEncoded []byte, publicKey *btcec.PublicKey, messageHash []byte) error { + if publicKey == nil || !publicKey.IsOnCurve() { + return errors.New("public key was not correctly instantiated") + } + + // Parse the signature so we can verify it + parsedSignature, err := ParseCompact(signatureEncoded) + if err != nil { + return err + } + + // Actually verify the message + if verified := parsedSignature.Verify(messageHash, publicKey); !verified { + return errors.New("signature could not be verified") + } + + return nil +} diff --git a/internal/generic/signature/signature_test.go b/internal/generic/signature/signature_test.go new file mode 100644 index 0000000..9a3798b --- /dev/null +++ b/internal/generic/signature/signature_test.go @@ -0,0 +1,140 @@ +package signature_test + +import ( + "math/big" + "reflect" + "testing" + "unsafe" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/stretchr/testify/suite" + + "github.com/bitonicnl/verify-signed-message/internal" + "github.com/bitonicnl/verify-signed-message/internal/generic/signature" +) + +type SignatureTestSuite struct { + suite.Suite + + signatureEncoded []uint8 + publicKeyEncoded []uint8 +} + +func (s *SignatureTestSuite) SetupTest() { + // uint8 representation of `H/iew/NhHV9V9MdUEn/LFOftaTy1ivGPKPKyMlr8OSokNC755fAxpSThNRivwTNsyY9vPUDTRYBPc2cmGd5d4y4=`. + s.signatureEncoded = []uint8{ + 31, 248, 158, 195, 243, 97, 29, 95, 85, 244, 199, 84, 18, 127, 203, 20, 231, 237, 105, 60, 181, + 138, 241, 143, 40, 242, 178, 50, 90, 252, 57, 42, 36, 52, 46, 249, 229, 240, 49, 165, 36, 225, 53, + 24, 175, 193, 51, 108, 201, 143, 111, 61, 64, 211, 69, 128, 79, 115, 103, 38, 25, 222, 93, 227, 46, + } + + // uint8 representation of the public key for `14dD6ygPi5WXdwwBTt1FBZK3aD8uDem1FY`. + s.publicKeyEncoded = []uint8{ + 3, 77, 160, 6, 249, 88, 190, 186, 120, 236, 84, 68, 61, 244, 163, 245, 34, 55, 37, 63, + 122, 232, 203, 219, 23, 220, 207, 63, 234, 165, 127, 49, 38, + } +} + +func TestServiceTestSuite(t *testing.T) { + // Run everything in parallel + t.Parallel() + + suite.Run(t, new(SignatureTestSuite)) +} + +func (s *SignatureTestSuite) TestParseCompactInvalid() { + compactedSignature, err := signature.ParseCompact([]byte{}) + s.Require().EqualError(err, "invalid compact signature size") + s.Nil(compactedSignature) +} + +func (s *SignatureTestSuite) TestParseCompactInvalidRecoveryCode() { + compactedSignature, err := signature.ParseCompact([]byte{ + 0x68, 0x1c, 0x6e, 0x8b, 0xac, 0xd1, 0x97, 0x25, + 0x18, 0x37, 0xcf, 0xe1, 0x7a, 0xa2, 0x07, 0x95, + 0x18, 0xd7, 0x1c, 0x3b, 0x68, 0x3b, 0x40, 0x20, + 0xf9, 0x3d, 0x64, 0x36, 0x2a, 0x0a, 0x63, 0x4e, + 0x17, 0x28, 0x2b, 0x47, 0x94, 0x16, 0x69, 0x6b, + 0xe2, 0x76, 0x7f, 0x02, 0xb2, 0x5e, 0xd1, 0x3c, + 0x43, 0x56, 0x4c, 0x28, 0xc3, 0x80, 0x5f, 0x6a, + 0x6a, 0x32, 0xe3, 0x0b, 0x6a, 0x26, 0xf0, 0x2f, + 0xc9, + }) + s.Require().EqualError(err, "invalid compact signature recovery code") + s.Nil(compactedSignature) +} + +func (s *SignatureTestSuite) TestParseCompact() { + compactedSignature, err := signature.ParseCompact(s.signatureEncoded) + s.Require().NoError(err) + + // Retrieve the unexported fields + R := s.getFieldFromSignature(compactedSignature, "r") + S := s.getFieldFromSignature(compactedSignature, "s") + + // Ensure they match what we defined + s.Equal("112454100686917088716763005039207074580155840372180209748670933598947425987108", R.String()) + s.Equal("23603267825273168310009216611640910854054822424267934178492474518750065713966", S.String()) +} + +func (s *SignatureTestSuite) TestVerifyInvalidPublicKey() { + err := signature.Verify(s.signatureEncoded, &btcec.PublicKey{}, []byte{}) + s.Require().EqualError(err, "public key was not correctly instantiated") +} + +func (s *SignatureTestSuite) TestVerifyInvalidEncodedSignature() { + key, err := btcec.NewPrivateKey() + s.Require().NoError(err) + + err = signature.Verify([]byte{}, key.PubKey(), []byte{}) + s.Require().EqualError(err, "invalid compact signature size") +} + +func (s *SignatureTestSuite) TestVerifyInvalidSignature() { + key, err := btcec.NewPrivateKey() + s.Require().NoError(err) + + err = signature.Verify(s.signatureEncoded, key.PubKey(), []byte{}) + s.Require().EqualError(err, "signature could not be verified") +} + +func (s *SignatureTestSuite) TestVerifyInvalidMessage() { + magicMessage := internal.CreateMagicMessage("INVALID") + messageHash := chainhash.DoubleHashB([]byte(magicMessage)) + + publicKey, err := btcec.ParsePubKey(s.publicKeyEncoded) + s.Require().NoError(err) + + s.Require().EqualError(signature.Verify(s.signatureEncoded, publicKey, messageHash), "signature could not be verified") +} + +func (s *SignatureTestSuite) TestVerify() { + magicMessage := internal.CreateMagicMessage("test message") + messageHash := chainhash.DoubleHashB([]byte(magicMessage)) + + publicKey, err := btcec.ParsePubKey(s.publicKeyEncoded) + s.Require().NoError(err) + + s.Require().NoError(signature.Verify(s.signatureEncoded, publicKey, messageHash)) +} + +func (s *SignatureTestSuite) getFieldFromSignature(compactedSignature *ecdsa.Signature, field string) *big.Int { + // Mark as helper + s.T().Helper() + + // Reflect the signature struct + elem := reflect.ValueOf(compactedSignature).Elem() + + // Grab the unexported field + rReflected := elem.FieldByName(field) + m, ok := reflect.NewAt(rReflected.Type(), unsafe.Pointer(rReflected.UnsafeAddr())).Elem().Interface().(btcec.ModNScalar) + s.True(ok) + + // Grab ModNScalar bytes + bytes := m.Bytes() + + // Convert field back to big.Int + return new(big.Int).SetBytes(bytes[:]) +} diff --git a/internal/generic/validation.go b/internal/generic/validation.go new file mode 100644 index 0000000..ba0513b --- /dev/null +++ b/internal/generic/validation.go @@ -0,0 +1,109 @@ +package generic + +import ( + "errors" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/samber/lo" + + "github.com/bitonicnl/verify-signed-message/internal/generic/flags" +) + +// ValidateP2PKH ensures that the passed P2PKH address matches the address generated from the public key hash, recovery flag and network. +func ValidateP2PKH(recoveryFlag int, pubkeyHash []byte, addr btcutil.Address, net *chaincfg.Params) (bool, error) { + // Ensure proper address type will be generated + if lo.Contains[int](flags.TrezorP2SHAndP2WPKH(), recoveryFlag) { + return false, errors.New("cannot use P2PKH for recovery flag 'BIP137 (Trezor) P2SH-P2WPKH'") + } else if lo.Contains[int](flags.TrezorP2WPKH(), recoveryFlag) { + return false, errors.New("cannot use P2PKH for recovery flag 'BIP137 (Trezor) P2WPKH'") + } + + // Generate the address and validate it + if p2pkhAddr, err := btcutil.NewAddressPubKeyHash(pubkeyHash, net); err != nil { + return false, err + } else if addr.String() != p2pkhAddr.String() { + return false, fmt.Errorf("generated address '%s' does not match expected address '%s'", p2pkhAddr.String(), addr.String()) + } + + return true, nil +} + +// ValidateP2SH ensures that the passed P2SH address matches the address generated from the public key hash, recovery flag and network. +func ValidateP2SH(recoveryFlag int, pubkeyHash []byte, addr btcutil.Address, net *chaincfg.Params) (bool, error) { + // Ensure proper address type will be generated + if lo.Contains[int](flags.Uncompressed(), recoveryFlag) { + return false, errors.New("cannot use P2SH for recovery flag 'P2PKH uncompressed'") + } else if lo.Contains[int](flags.TrezorP2WPKH(), recoveryFlag) { + return false, errors.New("cannot use P2SH for recovery flag 'BIP137 (Trezor) P2WPKH'") + } + + // Generate the address and validate it + if scriptSig, err := txscript.NewScriptBuilder().AddOp(txscript.OP_0).AddData(pubkeyHash).Script(); err != nil { + return false, err + } else if p2shAddr, err := btcutil.NewAddressScriptHash(scriptSig, net); err != nil { + return false, err + } else if addr.String() != p2shAddr.String() { + return false, fmt.Errorf("generated address '%s' does not match expected address '%s'", p2shAddr.String(), addr.String()) + } + + // Generate the address and validate it + if witnessPubKeyHash, err := btcutil.NewAddressWitnessPubKeyHash(pubkeyHash, net); err != nil { + return false, err + } else if witnessScript, err := txscript.PayToAddrScript(witnessPubKeyHash); err != nil { + return false, err + } else if p2shAddr, err := btcutil.NewAddressScriptHashFromHash(btcutil.Hash160(witnessScript), net); err != nil { + return false, err + } else if addr.String() != p2shAddr.String() { + return false, fmt.Errorf("generated address '%s' does not match expected address '%s'", p2shAddr.String(), addr.String()) + } + + return true, nil +} + +// ValidateP2WPKH ensures that the passed P2WPKH address matches the address generated from the public key hash, recovery flag and network. +func ValidateP2WPKH(recoveryFlag int, pubkeyHash []byte, addr btcutil.Address, net *chaincfg.Params) (bool, error) { + // Ensure proper address type will be generated + if lo.Contains[int](flags.Uncompressed(), recoveryFlag) { + return false, errors.New("cannot use P2WPKH for recovery flag 'P2PKH uncompressed'") + } + + // Generate the address and validate it + if p2wkhAddr, err := btcutil.NewAddressWitnessPubKeyHash(pubkeyHash, net); err != nil { + return false, err + } else if addr.String() != p2wkhAddr.String() { + return false, fmt.Errorf("generated address '%s' does not match expected address '%s'", p2wkhAddr.String(), addr.String()) + } + + return true, nil +} + +// ValidateP2TR ensures that the passed P2TR address matches the address generated from the public key hash, recovery flag and network. +// +// Only addresses without a tapscript are allowed becausethe verification is using the internal key. +func ValidateP2TR(recoveryFlag int, pubKey *btcec.PublicKey, addr btcutil.Address, net *chaincfg.Params) (bool, error) { + // Ensure proper address type will be generated + if lo.Contains[int](flags.TrezorP2SHAndP2WPKH(), recoveryFlag) { + return false, errors.New("cannot use P2TR for recovery flag 'BIP137 (Trezor) P2SH-P2WPKH'") + } else if lo.Contains[int](flags.TrezorP2WPKH(), recoveryFlag) { + return false, errors.New("cannot use P2TR for recovery flag 'BIP137 (Trezor) P2WPKH'") + } + + // Ensure proper public key + if _, err := schnorr.ParsePubKey(schnorr.SerializePubKey(pubKey)); err != nil { + return false, err + } + + // Generate the address and validate it + if p2trAddr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(txscript.ComputeTaprootKeyNoScript(pubKey)), net); err != nil { + return false, fmt.Errorf("could not create taproot address: %w", err) + } else if addr.String() != p2trAddr.String() { + return false, fmt.Errorf("generated address '%s' does not match expected address '%s'", p2trAddr.String(), addr.String()) + } + + return true, nil +} diff --git a/internal/generic/validation_test.go b/internal/generic/validation_test.go new file mode 100644 index 0000000..0a04ce1 --- /dev/null +++ b/internal/generic/validation_test.go @@ -0,0 +1,276 @@ +package generic_test + +import ( + "crypto/rand" + "encoding/hex" + "errors" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/bitonicnl/verify-signed-message/internal/generic" +) + +type ValidateTestSuite struct { + suite.Suite + + legacyPubKeyHash []uint8 + compressedPublicKey *btcec.PublicKey + uncompressedPublicKey *btcec.PublicKey +} + +func TestValidateTestSuite(t *testing.T) { + // Run everything in parallel + t.Parallel() + + suite.Run(t, new(ValidateTestSuite)) +} + +func (s *ValidateTestSuite) SetupTest() { + // Compressed legacy public key in bytes (1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5) + s.legacyPubKeyHash = []uint8{133, 113, 93, 2, 177, 222, 121, 165, 69, 34, 61, 182, 122, 239, 165, 136, 229, 124, 167, 194} + + // Compressed taproot public key in hexadecimal format (bc1pgc9k3vdmr9aecmwj09qg5qv550qyyrydufyfmxrsvk5474rxenuqrq4lcz) + s.compressedPublicKey = s.createTaprootPublicKey("0296f45e80c8efdb88b544afde38f2a19d65d40086cff9e2fdd5868d5eb57ca8a6") + + // Uncompressed taproot public key in hexadecimal format (bc1pg48rw0vphy9mght5dr8s5prx92a44wpqmzk67xk8yjf5zlancj9sa3plhc) + s.uncompressedPublicKey = s.createTaprootPublicKey("04c78ea05297a242ba0b2b105bed475b8796fcea30638813f35989c4e0f1df9ef6") +} + +func (s *ValidateTestSuite) TestValidateP2PKH() { + type args struct { + recoveryFlag int + pubKeyHash []byte + addr btcutil.Address + } + tests := []struct { + name string + args args + want error + }{ + { + name: "Invalid recovery flag - TrezorP2SHAndP2WPKH", + args: args{recoveryFlag: 35, pubKeyHash: []uint8{}, addr: &RandomAddress{}}, + want: errors.New("cannot use P2PKH for recovery flag 'BIP137 (Trezor) P2SH-P2WPKH'"), + }, + { + name: "Invalid recovery flag - TrezorP2WPKH", + args: args{recoveryFlag: 39, pubKeyHash: []uint8{}, addr: &RandomAddress{}}, + want: errors.New("cannot use P2PKH for recovery flag 'BIP137 (Trezor) P2WPKH'"), + }, + { + name: "Invalid PubKeyHash", + args: args{recoveryFlag: 32, pubKeyHash: []uint8{}, addr: &RandomAddress{}}, + want: errors.New("pkHash must be 20 bytes"), + }, + { + name: "Invalid address for public key hash", + args: args{recoveryFlag: 32, pubKeyHash: s.legacyPubKeyHash, addr: &RandomAddress{Address: "Invalid"}}, + want: errors.New("generated address '1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5' does not match expected address 'Invalid'"), + }, + { + name: "Valid P2PKH", + args: args{recoveryFlag: 32, pubKeyHash: s.legacyPubKeyHash, addr: &RandomAddress{Address: "1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5"}}, + want: nil, + }, + } + + for _, tt := range tests { + s.T().Run(tt.name, func(t *testing.T) { + _, err := generic.ValidateP2PKH(tt.args.recoveryFlag, tt.args.pubKeyHash, tt.args.addr, &chaincfg.MainNetParams) + require.Equal(t, tt.want, err) + }) + } +} + +func (s *ValidateTestSuite) TestValidateP2SH() { + pubKeyHashTooLong := make([]uint8, txscript.MaxScriptSize+2) + _, err := rand.Read(pubKeyHashTooLong) + s.Require().NoError(err) + + type args struct { + recoveryFlag int + pubKeyHash []byte + addr btcutil.Address + } + tests := []struct { + name string + args args + want error + }{ + { + name: "Invalid recovery flag - Uncompressed", + args: args{recoveryFlag: 27, pubKeyHash: []uint8{}, addr: &RandomAddress{}}, + want: errors.New("cannot use P2SH for recovery flag 'P2PKH uncompressed'"), + }, + { + name: "Invalid recovery flag - TrezorP2WPKH", + args: args{recoveryFlag: 39, pubKeyHash: []uint8{}, addr: &RandomAddress{}}, + want: errors.New("cannot use P2SH for recovery flag 'BIP137 (Trezor) P2WPKH'"), + }, + { + name: "Invalid pubKeyHash - Too long", + args: args{recoveryFlag: 35, pubKeyHash: pubKeyHashTooLong, addr: &RandomAddress{Address: "Invalid"}}, + want: txscript.ErrScriptNotCanonical("adding 10005 bytes of data would exceed the maximum allowed canonical script length of 10000"), + }, + { + name: "Invalid address for public key hash", + args: args{recoveryFlag: 35, pubKeyHash: s.legacyPubKeyHash, addr: &RandomAddress{Address: "Invalid"}}, + want: errors.New("generated address '3Nxee1CFDqFRtUrixREpNMhsmH9TBXcY48' does not match expected address 'Invalid'"), + }, + { + name: "Valid P2SH", + args: args{recoveryFlag: 35, pubKeyHash: s.legacyPubKeyHash, addr: &RandomAddress{Address: "3Nxee1CFDqFRtUrixREpNMhsmH9TBXcY48"}}, + want: nil, + }, + } + + for _, tt := range tests { + s.T().Run(tt.name, func(t *testing.T) { + _, err := generic.ValidateP2SH(tt.args.recoveryFlag, tt.args.pubKeyHash, tt.args.addr, &chaincfg.MainNetParams) + require.Equal(t, tt.want, err) + }) + } +} + +func (s *ValidateTestSuite) TestValidateP2WPKH() { + type args struct { + recoveryFlag int + witnessProg []byte + addr btcutil.Address + } + tests := []struct { + name string + args args + want error + }{ + { + name: "Invalid recovery flag - Uncompressed", + args: args{recoveryFlag: 27, witnessProg: []uint8{}, addr: &RandomAddress{}}, + want: errors.New("cannot use P2WPKH for recovery flag 'P2PKH uncompressed'"), + }, + { + name: "Invalid witness program", + args: args{recoveryFlag: 32, witnessProg: []uint8{}, addr: &RandomAddress{}}, + want: errors.New("witness program must be 20 bytes for p2wpkh"), + }, + { + name: "Invalid address for public key hash", + args: args{recoveryFlag: 32, witnessProg: s.legacyPubKeyHash, addr: &RandomAddress{Address: "Invalid"}}, + want: errors.New("generated address 'bc1qs4c46q43meu623fz8km84ma93rjhef7z88rg99' does not match expected address 'Invalid'"), + }, + { + name: "Valid P2WPKH", + args: args{recoveryFlag: 32, witnessProg: s.legacyPubKeyHash, addr: &RandomAddress{Address: "bc1qs4c46q43meu623fz8km84ma93rjhef7z88rg99"}}, + want: nil, + }, + } + + for _, tt := range tests { + s.T().Run(tt.name, func(t *testing.T) { + _, err := generic.ValidateP2WPKH(tt.args.recoveryFlag, tt.args.witnessProg, tt.args.addr, &chaincfg.MainNetParams) + require.Equal(t, tt.want, err) + }) + } +} + +// All addresses were generated via https://demo.unisat.io/ +func (s *ValidateTestSuite) TestValidateP2TR() { + type args struct { + recoveryFlag int + pubKey *btcec.PublicKey + addr btcutil.Address + } + tests := []struct { + name string + args args + want error + }{ + { + name: "Invalid recovery flag - TrezorP2WPKH", + args: args{recoveryFlag: 36, pubKey: &btcec.PublicKey{}, addr: &RandomAddress{}}, + want: errors.New("cannot use P2TR for recovery flag 'BIP137 (Trezor) P2SH-P2WPKH'"), + }, + { + name: "Invalid recovery flag - TrezorP2WPKH", + args: args{recoveryFlag: 39, pubKey: &btcec.PublicKey{}, addr: &RandomAddress{}}, + want: errors.New("cannot use P2TR for recovery flag 'BIP137 (Trezor) P2WPKH'"), + }, + { + name: "Invalid public key", + args: args{recoveryFlag: 27, pubKey: btcec.NewPublicKey(&btcec.FieldVal{}, &btcec.FieldVal{}), addr: &RandomAddress{}}, + want: secp256k1.Error{Err: secp256k1.ErrPubKeyNotOnCurve, Description: "invalid public key: x coordinate 0000000000000000000000000000000000000000000000000000000000000000 is not on the secp256k1 curve"}, + }, + { + name: "Invalid address for public key - compressed", + args: args{recoveryFlag: 31, pubKey: s.compressedPublicKey, addr: &RandomAddress{Address: "Invalid"}}, + want: errors.New("generated address 'bc1pgc9k3vdmr9aecmwj09qg5qv550qyyrydufyfmxrsvk5474rxenuqrq4lcz' does not match expected address 'Invalid'"), + }, + { + name: "Invalid address for public key", + args: args{recoveryFlag: 27, pubKey: s.uncompressedPublicKey, addr: &RandomAddress{Address: "Invalid"}}, + want: errors.New("generated address 'bc1pg48rw0vphy9mght5dr8s5prx92a44wpqmzk67xk8yjf5zlancj9sa3plhc' does not match expected address 'Invalid'"), + }, + { + name: "Valid P2TR - compressed", + args: args{recoveryFlag: 31, pubKey: s.compressedPublicKey, addr: &RandomAddress{Address: "bc1pgc9k3vdmr9aecmwj09qg5qv550qyyrydufyfmxrsvk5474rxenuqrq4lcz"}}, + want: nil, + }, + { + name: "Valid P2TR", + args: args{recoveryFlag: 27, pubKey: s.uncompressedPublicKey, addr: &RandomAddress{Address: "bc1pg48rw0vphy9mght5dr8s5prx92a44wpqmzk67xk8yjf5zlancj9sa3plhc"}}, + want: nil, + }, + } + + for _, tt := range tests { + s.T().Run(tt.name, func(t *testing.T) { + _, err := generic.ValidateP2TR(tt.args.recoveryFlag, tt.args.pubKey, tt.args.addr, &chaincfg.MainNetParams) + require.Equal(t, tt.want, err) + }) + } +} + +func (s *ValidateTestSuite) createTaprootPublicKey(publicKey string) *btcec.PublicKey { + // Convert hexadecimal to bytes + compressedPublicKeyBytes, err := hex.DecodeString(publicKey[2:]) + if err != nil { + s.Require().NoError(err) + } + + // Setup X,Y storage + x, y := &btcec.FieldVal{}, &btcec.FieldVal{} + + // Since taproot uses x-only public keys, only set X + x.SetBytes((*[32]byte)(compressedPublicKeyBytes[:32])) + + // Create a btcd public key + return btcec.NewPublicKey(x, y) +} + +// RandomAddress implements the btcutil.Address interface and serves as a no-op to test these calls. +type RandomAddress struct { + Address string +} + +func (b *RandomAddress) EncodeAddress() string { + return b.Address +} + +func (b *RandomAddress) ScriptAddress() []byte { + return nil +} + +func (b *RandomAddress) IsForNet(_ *chaincfg.Params) bool { + return true +} + +func (b *RandomAddress) String() string { + return b.Address +} diff --git a/internal/generic/verify.go b/internal/generic/verify.go new file mode 100644 index 0000000..061157c --- /dev/null +++ b/internal/generic/verify.go @@ -0,0 +1,82 @@ +package generic + +import ( + "errors" + "fmt" + "reflect" + + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/samber/lo" + + "github.com/bitonicnl/verify-signed-message/internal" + "github.com/bitonicnl/verify-signed-message/internal/generic/flags" + "github.com/bitonicnl/verify-signed-message/internal/generic/signature" +) + +// ExpectedSignatureLength contains the fixed signature length all signed messages are expected to have. +const ExpectedSignatureLength = 65 + +func Verify(address btcutil.Address, message string, signatureDecoded []byte, net *chaincfg.Params) (bool, error) { + // Ensure signature has proper length + if len(signatureDecoded) != ExpectedSignatureLength { + return false, fmt.Errorf("wrong signature length: %d instead of %d", len(signatureDecoded), ExpectedSignatureLength) + } + + // Ensure signature has proper recovery flag + recoveryFlag := int(signatureDecoded[0]) + if !lo.Contains[int](flags.All(), recoveryFlag) { + return false, fmt.Errorf("invalid recovery flag: %d", recoveryFlag) + } + + // Should address be compressed (for checking later) + compressed := flags.ShouldBeCompressed(recoveryFlag) + + // Reset recovery flag after obtaining keyID for Trezor + if lo.Contains[int](flags.Trezor(), recoveryFlag) { + signatureDecoded[0] = byte(27 + flags.GetKeyID(recoveryFlag)) + } + + // Make and hash the message + messageHash := chainhash.DoubleHashB([]byte(internal.CreateMagicMessage(message))) + + // Recover the public key from signature and message hash + publicKey, wasCompressed, err := ecdsa.RecoverCompact(signatureDecoded, messageHash) + if err != nil { + return false, fmt.Errorf("could not recover pubkey: %w", err) + } + + // Ensure our initial assumption was correct, except for Trezor as they do something different + if compressed != wasCompressed && !lo.Contains[int](flags.Trezor(), recoveryFlag) { + return false, errors.New("we expected the key to be compressed, it wasn't") + } + + // Verify that the signature is valid + // TODO: ecdsa.RecoverCompact already does all, check if we can just remove it + if err := signature.Verify(signatureDecoded, publicKey, messageHash); err != nil { + return false, err + } + + // Get the hash from the public key, so we can check that address matches + publicKeyHash := GeneratePublicKeyHash(recoveryFlag, publicKey) + + switch address.(type) { + // Validate P2PKH - Legacy + case *btcutil.AddressPubKeyHash: + return ValidateP2PKH(recoveryFlag, publicKeyHash, address, net) + // Validate P2SH-P2WPKH - Segwit + case *btcutil.AddressScriptHash: + return ValidateP2SH(recoveryFlag, publicKeyHash, address, net) + // Validate P2WPKH - Native Segwit + case *btcutil.AddressWitnessPubKeyHash: + return ValidateP2WPKH(recoveryFlag, publicKeyHash, address, net) + // Validate P2TR - Taproot + case *btcutil.AddressTaproot: + return ValidateP2TR(recoveryFlag, publicKey, address, net) + // Unsupported address + default: + return false, fmt.Errorf("unsupported address type '%s'", reflect.TypeOf(address)) + } +} diff --git a/internal/generic/verify_test.go b/internal/generic/verify_test.go new file mode 100644 index 0000000..44a4769 --- /dev/null +++ b/internal/generic/verify_test.go @@ -0,0 +1,311 @@ +package generic_test + +import ( + "encoding/base64" + "strings" + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/stretchr/testify/suite" + + "github.com/bitonicnl/verify-signed-message/internal/generic" + verifier "github.com/bitonicnl/verify-signed-message/pkg" +) + +type VerifyTestSuite struct { + suite.Suite +} + +func TestVerifyTestSuite(t *testing.T) { + // Run everything in parallel + t.Parallel() + + suite.Run(t, new(VerifyTestSuite)) +} + +func (s *VerifyTestSuite) TestVerifyIncorrect() { + tests := map[string]struct { + signedMessage verifier.SignedMessage + expectedError string + }{ + "address - different": { + signedMessage: verifier.SignedMessage{ + Address: "14wPe34dikRzK4tMYvtwMMJCEZbJ7ar35V", + Message: "test message", + Signature: "IFqUo4/sxBEFkfK8mZeeN56V13BqOc0D90oPBChF3gTqMXtNSCTN79UxC33kZ8Mi0cHy4zYCnQfCxTyLpMVXKeA=", + }, + expectedError: "generated address '1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5' does not match expected address '14wPe34dikRzK4tMYvtwMMJCEZbJ7ar35V'", + }, + // Checksum mismatch, taken from https://github.com/scintill/php-bitcoin-signature-routines/blob/master/test/verifymessage.php#L32 + "message - different": { + signedMessage: verifier.SignedMessage{ + Address: "14wPe34dikRzK4tMYvtwMMJCEZbJ7ar35V", + Message: "Totally different message, thus different calculated address", + Signature: "IFqUo4/sxBEFkfK8mZeeN56V13BqOc0D90oPBChF3gTqMXtNSCTN79UxC33kZ8Mi0cHy4zYCnQfCxTyLpMVXKeA=", + }, + expectedError: "generated address '1LwzpMrpDakgZ9XPCsSuGG6ZXCE3fkNdQR' does not match expected address '14wPe34dikRzK4tMYvtwMMJCEZbJ7ar35V'", + }, + // Incorrect signature that is valid, but cannot be recovered, taken from https://github.com/scintill/php-bitcoin-signature-routines/blob/master/test/verifymessage.php#L100 + "signature - invalid curve": { + signedMessage: verifier.SignedMessage{ + Address: "1C9CRMGBYrGKKQ6eEpwm4dzMqkRZxPB5xa", + Message: "test", + Signature: "IQt3ycjmA6LCbcTiFcj7o6odqX5PKeYPmL+dwcblLc/Xor1E2szTlEZKtHdzSrSz78PbYQUlX5a5VuDeSJLrEr0=", + }, + expectedError: "could not recover pubkey: invalid signature: signature R + N >= P", + }, + "signature - non-bitcoin": { + signedMessage: verifier.SignedMessage{ + Address: "1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5", + Message: "test message", + Signature: "zPOBbkXzwDgGVU3Gxk0noVuLq8P1pGfQUxnS0nzuxEN3qR/U/s63P81io7LV04ZxN88gVX/Qw0rzLFBR8q4IkUc=", + }, + expectedError: "invalid recovery flag: 204", + }, + "signature - too short": { + signedMessage: verifier.SignedMessage{ + Address: "1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5", + Message: "test message", + Signature: "VGhpcyBpcyBub3QgdmFsaWQ=", + }, + expectedError: "wrong signature length: 17 instead of 65", + }, + // Generated via https://demo.unisat.io/ and has an invalid recovery flag, which causes it to be generated uncompressed (the address is compressed). + // 2024-03-23: This issue seems to have been resolved on their end. + "unisat - P2PKH": { + signedMessage: verifier.SignedMessage{ + Address: "15tbg628HntFEB7xjyVrSo3ck5jbKuGhQD", + Message: "hello world", + Signature: "G5WBoAY8ehQtP8UnS2boqjid2vYxH2/m69Il3T1SySRGVO2H1KIrTwVkPe2aU3BXyX/CYzBUaXYyWmC8vxXFIyw=", + }, + expectedError: "generated address '1NAnF6TPUieShRuhVyK5nYAGpvGwXSS7RX' does not match expected address '15tbg628HntFEB7xjyVrSo3ck5jbKuGhQD'", + }, + // Generated via https://demo.unisat.io/ and has an invalid recovery flag. + // 2024-03-23: This issue seems to have been resolved on their end. + "unisat - P2SH-P2WPKH": { + signedMessage: verifier.SignedMessage{ + Address: "32ypXz5xwzGLbEnfLJWw1VUKcLbvDDVTVV", + Message: "hello world", + Signature: "HEZseoQ4aMFs8ERwwB9jm4qgoUH/sFRMTEADV9pr5EQadve7ebbsQ/LH/c7QpnDY/ygi24jlnPoZUcOT7Vo8vOw=", + }, + expectedError: "cannot use P2SH for recovery flag 'P2PKH uncompressed'", + }, + // Generated via https://demo.unisat.io/ and has an invalid recovery flag. + // 2024-03-23: This issue seems to have been resolved on their end. + "unisat - P2WPKH": { + signedMessage: verifier.SignedMessage{ + Address: "bc1qzex95t5x94sq70g8u7zyc5jcn6vv27swtm5uqs", + Message: "hello world", + Signature: "HCxsLSgGi9RduaXTTzQvbpTNVR/KyWX9Rk4SU0LnhXN8T+A+8titHwMZea2PiOSQzfSu2J+og307rEw2GRZDeDE=", + }, + expectedError: "cannot use P2WPKH for recovery flag 'P2PKH uncompressed'", + }, + } + + for name, tt := range tests { + s.Run(name, func() { + // Decode the address + address, err := btcutil.DecodeAddress(tt.signedMessage.Address, &chaincfg.MainNetParams) + s.Require().NoError(err) + + // Decode the signature + signatureDecoded, err := base64.StdEncoding.DecodeString(tt.signedMessage.Signature) + s.Require().NoError(err) + + valid, err := generic.Verify(address, tt.signedMessage.Message, signatureDecoded, &chaincfg.MainNetParams) + s.Require().EqualError(err, tt.expectedError) + s.False(valid) + }) + } +} + +func (s *VerifyTestSuite) TestVerify() { + tests := map[string]verifier.SignedMessage{ + "bitcoin core - legacy": { + Address: "1CBHFokbnZVuq9fA3yjPTvSNXpdRRP7eUB", + Message: " Lorem ipsum dolor sit amet, consectetur adipiscing elit. In a turpis dignissim, tincidunt dolor quis, aliquam justo. Sed eleifend eleifend tempus. Sed blandit lectus at ullamcorper blandit. Quisque suscipit ligula lacus, tempor fringilla erat pharetra a. Curabitur pretium varius purus vel luctus. Donec fringilla velit vel risus fermentum, ac aliquam enim sollicitudin. Aliquam elementum, nunc nec malesuada fringilla, sem sem lacinia libero, id tempus nunc velit nec dui. Vestibulum gravida non tortor sit amet accumsan. Nunc semper vehicula vestibulum. Praesent at nibh dapibus, eleifend neque vitae, vehicula justo. Nam ultricies at orci vel laoreet. Morbi metus sapien, pulvinar ut dui ut, malesuada lobortis odio. Curabitur eget diam ligula. Nunc vel nisl consectetur, elementum magna et, elementum erat. Maecenas risus massa, mattis a sapien sed, molestie ullamcorper sapien. ", + Signature: "H3HQ9gwAMCee0T7M8fZTgvIYlG6pMnpP41ioDUTKjlPsOMHwrF3qmgsM+kFoWLL1u6P4ZUf3nwYacPCeBjrzFzE=", + }, + // Taken from https://github.com/btclib-org/btclib/blob/v2022.7.20/tests/ecc/test_bms.py + "bms compressed - legacy": { + Address: "14dD6ygPi5WXdwwBTt1FBZK3aD8uDem1FY", + Message: "test message", + Signature: "H/iew/NhHV9V9MdUEn/LFOftaTy1ivGPKPKyMlr8OSokNC755fAxpSThNRivwTNsyY9vPUDTRYBPc2cmGd5d4y4=", + }, + // Taken from https://github.com/btclib-org/btclib/blob/v2022.7.20/tests/ecc/test_bms.py + "bms compressed p2pkh": { + Address: "1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5", + Message: "test message", + Signature: "IFqUo4/sxBEFkfK8mZeeN56V13BqOc0D90oPBChF3gTqMXtNSCTN79UxC33kZ8Mi0cHy4zYCnQfCxTyLpMVXKeA=", + }, + // Taken from https://github.com/btclib-org/btclib/blob/v2022.7.20/tests/ecc/test_bms.py + "bms uncompressed - legacy": { + Address: "1HUBHMij46Hae75JPdWjeZ5Q7KaL7EFRSD", + Message: "test message", + Signature: "G/iew/NhHV9V9MdUEn/LFOftaTy1ivGPKPKyMlr8OSokNC755fAxpSThNRivwTNsyY9vPUDTRYBPc2cmGd5d4y4=", + }, + // Taken from https://github.com/btclib-org/btclib/blob/v2022.7.20/tests/ecc/test_bms.py + "bms uncompressed p2pkh": { + Address: "19f7adDYqhHSJm2v7igFWZAqxXHj1vUa3T", + Message: "test message", + Signature: "HFqUo4/sxBEFkfK8mZeeN56V13BqOc0D90oPBChF3gTqMXtNSCTN79UxC33kZ8Mi0cHy4zYCnQfCxTyLpMVXKeA=", + }, + "coinomi - legacy": { + Address: "1PjSDaSiVdWW6YjwFA6FHwwfqkZdPEJUZv", + Message: "Test message!", + Signature: "IK7I33rASHdSeYDotQ9WfO4jrxgdl5ef/bTbX6Q5PNtFY9rJeAHfoZV5GpDO1K3OqoPs8ROZRXPyMNLkVOxJ+Rc=", + }, + "coinomi - segwit": { + Address: "39FT3L2wH56h2jmae5abPU1A7nVs6QyApV", + Message: "Test message!", + Signature: "HzpoLFjr+eUPkseb+i0Vaqj7FRm5o1+Ei/kae7XWN6nmFLmvLi7uWicerYNXjCMUf3nCnm/9UPb6SYJLI60Nh8A=", + }, + "coinomi - segwit native": { + Address: "bc1q0utxws6ptfdfcvaz29y4st065t5ku6vcqd364f", + Message: "Test message!", + Signature: "H+G7Fz3EVxX02kIker4HPgnP8Mlf3bT52p81hnNAahTOGJ8ANSaU0bF5RsprgTH6LXLx/PmCka48Ov7OrPw2bms=", + }, + "electrum - legacy": { + Address: "1CPBDkm8ER3o7r2HANcvNoVHsBYKcUHTp9", + Message: "Integer can be encoded depending on the represented value to save space. Variable length integers always precede an array/vector of a type of data that may vary in length. Longer numbers are encoded in little endian. If you're reading the Satoshi client code (BitcoinQT) it refers to this encoding as a \"CompactSize\". Modern Bitcoin Core also has the VARINT macro which implements an even more compact integer for the purpose of local storage (which is incompatible with \"CompactSize\" described here). VARINT is not a part of the protocol.", + Signature: "IHTr8YSzZ17Ut/Qaaui6BvGd42+TGwVwNYaIMUAZQTZRSqDtaTfsOcaOllPstp3IxzMlpXVOzLxNZE8r8ieffnY=", + }, + "electrum - segwit native": { + Address: "bc1qsdjne3y6ljndzvg9z9qrhje8k7p2m5yas704hn", + Message: "Integer can be encoded depending on the represented value to save space. Variable length integers always precede an array/vector of a type of data that may vary in length. Longer numbers are encoded in little endian. If you're reading the Satoshi client code (BitcoinQT) it refers to this encoding as a \"CompactSize\". Modern Bitcoin Core also has the VARINT macro which implements an even more compact integer for the purpose of local storage (which is incompatible with \"CompactSize\" described here). VARINT is not a part of the protocol.", + Signature: "H3TkHAXCKRfyDowCra5YRDF/Vkk2HQCel/pgEgTj9LYaWpnviSRcuYtv/CZk7NTyHsJnYP56bqbvuU3PejwLCnA=", + }, + "mycelium - legacy": { + Address: "13VwTBVLNpNSQVTrYpuHQVJYnk2y2Nr1ue", + Message: "Test message!", + Signature: "Hxpnr2oDFTjivFkrrp89UoMrzaAzFkkEciS3MUHCfdoEXN/KvHi9ii2Xz+FuQ6KjlZDlaPb197E8TWnhIAzbT0M=", + }, + "mycelium - segwit": { + Address: "325ZMWMu9vaWQeUG8Gc8MzsVKzt3Rqn8H7", + Message: "Test message!", + Signature: "IM/bkqpERGRFDGgxnceinULcqz1iRVBSUVlnDPZRKHGUQMC5t1P5wRp2/1b1+rpjFHhSS2pExB88cA750PNRlaw=", + }, + "mycelium - segwit native": { + Address: "bc1q58dh2fpwms37g29nw979pa65lsvjkqxq82jzvv", + Message: "Test message!", + Signature: "ILNax/LC+m3WwzIhnrieNN8DRzWTAgcVStSJmwdabUQII2fIlYUlEgnlNf4j2G4yJQoO4zFqCwaLOX4PDj1XwjA=", + }, + // Taken from https://github.com/petertodd/python-bitcoinlib/blob/master/bitcoin/tests/test_signmessage.py + "python-bitcoinlib - legacy": { + Address: "1F26pNMrywyZJdr22jErtKcjF8R3Ttt55G", + Message: "1F26pNMrywyZJdr22jErtKcjF8R3Ttt55G", + Signature: "H85WKpqtNZDrajOnYDgUY+abh0KCAcOsAIOQwx2PftAbLEPRA7mzXA/CjXRxzz0MC225pR/hx02Vf2Ag2x33kU4=", + }, + // Dumped from (changed TestNet to MainNet): https://github.com/Samourai-Wallet/ExtLibJ/blob/develop/src/test/java/com/samourai/wallet/util/MessageSignUtilGenericTest.java + "samourai - legacy": { + Address: "1JSjyW3dZSQHv6jb6u6baXLUZnsThqmzf4", + Message: "hello foo", + Signature: "INAP+PMyI2vqIxiEKIcPOaaffspU3gAPm0YWhCxJr5iqWbQwqns9+RiXIzuU9JoNQs/MQ1BZ4O2XM23utyw3jr0=", + }, + // Dumped from (changed TestNet to MainNet): https://github.com/Samourai-Wallet/ExtLibJ/blob/develop/src/test/java/com/samourai/wallet/util/MessageSignUtilGenericTest.java + "samourai - segwit native": { + Address: "bc1qnxhkjd3kcjdzqz4u0m47xj3dne907cd8yg7qdr", + Message: "hello foo", + Signature: "IJruGdQX+V6s+zvzTD3msz2l1obPchx19/bsefr+QGihcRArLSzXtkoUXA8k0NkBsIpFXGRxbG/s+eimZ+eGg70=", + }, + // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py + "trezor - legacy": { + Address: "1JAd7XCBzGudGpJQSDSfpmJhiygtLQWaGL", + Message: "This is an example of a signed message.", + Signature: "IP2PL321I4/N0HfVIEw+aUnCYdcAJpzvwdnS3O9rlQI2MO5hf2yKz560DI7dcEycp06kr8OT9D81tOiVgyTL3Rw=", + }, + // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py + "trezor - legacy - long message": { + Address: "1JAd7XCBzGudGpJQSDSfpmJhiygtLQWaGL", + Message: strings.Repeat("VeryLongMessage!", 64), + Signature: "IApGR2zrhNBu9XhIKAJvkiyIFfV6rIN7jAEwB8qKhGDbY++Rfb6669EIscgUu+6m2x8rIkGpWOU/5xXMhrGZ2cM=", + }, + // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py + "trezor - segwit native": { + Address: "bc1qannfxke2tfd4l7vhepehpvt05y83v3qsf6nfkk", + Message: "This is an example of a signed message.", + Signature: "KLVddgDZ6afipJFV3fPP2455bCB/qrgzAQ+kH7eCiIm8R89iNIp6qgkjwIMqWJ+rVB6PEutU+3EckOIwfw9msZQ=", + }, + // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py + "trezor - segwit native long message": { + Address: "bc1qannfxke2tfd4l7vhepehpvt05y83v3qsf6nfkk", + Message: strings.Repeat("VeryLongMessage!", 64), + Signature: "KMb4biVeqnaMRH1jXZHaAWMaxUryI8LBgtT6NnbP7K5KGZrTOnT+BPtGw5QyrLjYPedNqQ9fARI7O32LwlK8f3E=", + }, + // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py + "trezor - segwit - #0": { + Address: "3L6TyTisPBmrDAj6RoKmDzNnj4eQi54gD2", + Message: "This is an example of a signed message.", + Signature: "I3RN5FFvrFwUCAgBVmRRajL+rZTeiXdc7H4k28JP4TMHWsCTAcTMjhl76ktkgWYdW46b8Z2Le4o4Ls21PC7gdQ0=", + }, + // Taken from https://github.com/bitcoinjs/bitcoinjs-message/issues/20 + "trezor - segwit - #1": { + Address: "3LbZqMMHu371r5Fjve9qNhSQzuNi7EzqUR", + Message: "test123", + Signature: "I2ehXowFWMZohHrJN+1IRdDwqN/UILqVmhIOHpeBdS4BYDCQpfDL1tTH7mNg6eeypno+Is8ApgWinkPnnz1NEq8=", + }, + // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py + "trezor - segwit - long message": { + Address: "3L6TyTisPBmrDAj6RoKmDzNnj4eQi54gD2", + Message: strings.Repeat("VeryLongMessage!", 64), + Signature: "I26t7jgGhPcHScUhQciqfDtq/YTQ5fOM+nGCPzsRBaXzTiODSlu28jn/KK2H9An0TkzmJpdUrcADiLGVB6XZOG8=", + }, + // Taken from https://github.com/bitcoinjs/bitcoinjs-message/issues/20 + "electrum - segwit": { + Address: "3LbZqMMHu371r5Fjve9qNhSQzuNi7EzqUR", + Message: "test123", + Signature: "H2ehXowFWMZohHrJN+1IRdDwqN/UILqVmhIOHpeBdS4BYDCQpfDL1tTH7mNg6eeypno+Is8ApgWinkPnnz1NEq8=", + }, + "electrum - legacy - short message - uncompressed": { + Address: "18J72YSM9pKLvyXX1XAjFXA98zeEvxBYmw", + Message: "Test123", + Signature: "Gzhfsw0ItSrrTCChykFhPujeTyAcvVxiXwywxpHmkwFiKuUR2ETbaoFcocmcSshrtdIjfm8oXlJoTOLosZp3Yc8=", + }, + "electrum - legacy - long message - uncompressed": { + Address: "18J72YSM9pKLvyXX1XAjFXA98zeEvxBYmw", + Message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In a turpis dignissim, tincidunt dolor quis, aliquam justo. Sed eleifend eleifend tempus. Sed blandit lectus at ullamcorper blandit. Quisque suscipit ligula lacus, tempor fringilla erat pharetra a. Curabitur pretium varius purus vel luctus. Donec fringilla velit vel risus fermentum, ac aliquam enim sollicitudin. Aliquam elementum, nunc nec malesuada fringilla, sem sem lacinia libero, id tempus nunc velit nec dui. Vestibulum gravida non tortor sit amet accumsan. Nunc semper vehicula vestibulum. Praesent at nibh dapibus, eleifend neque vitae, vehicula justo. Nam ultricies at orci vel laoreet. Morbi metus sapien, pulvinar ut dui ut, malesuada lobortis odio. Curabitur eget diam ligula. Nunc vel nisl consectetur, elementum magna et, elementum erat. Maecenas risus massa, mattis a sapien sed, molestie ullamcorper sapien.", + Signature: "HHOGSz6AUEEyVGoCUw1GqQ5qy9KvW5uO1FfqWLbwYxkQVsI+sbM0jpBQWkyjr72166yiL/LQEtW3SpVBR1gXdYY=", + }, + // Generated via https://demo.unisat.io/ + "unisat - legacy": { + Address: "19892aZkySq8Va3Qp2gk9dapNtWypnL1ek", + Message: "hello world~", + Signature: "IOmbxBO4Wwy42+Q9JoOB2ZaXygdDCIaGKloc5igs+ZF0WqWIVDiFLmuUZKdGeSiz+VNPd19d4hPHgOXCfZfZveQ=", + }, + // Generated via https://demo.unisat.io/ + "unisat - nested segwit": { + Address: "3MqrCDcTK16rkLEbu9Wfojdf7jbUMAJBRW", + Message: "hello world~", + Signature: "IGCFq01RhaGbMOOuPWFn5H/ZCKx+P4srkb7O3BRS7mcISazqQbS9QThf3gSmH1Vrq/RLL+1pZQZISWe/XRIr1O0=", + }, + // Generated via https://demo.unisat.io/ + "unisat - segwit native": { + Address: "bc1qvhnxd953tzt4kcqpcgk83wu2r9shf59q2t4egu", + Message: "hello world", + Signature: "INuYn+2RZPLOzOXBYffJeMSVKRwIf+XaD3SrQa+WTO/aaKR/+JzS0zJplnc3H7dN8Da3bxvrQx3rPL/MCwZ5z7s=", + }, + // Generated via https://demo.unisat.io/ + "unisat - taproot": { + Address: "bc1pgc9k3vdmr9aecmwj09qg5qv550qyyrydufyfmxrsvk5474rxenuqrq4lcz", + Message: "hello world", + Signature: "H/KLWcCfl/P34V9TdPzcSlG3sdhllArBXjypbz9BBY1GXDRCwYogO50Crznm8I9P/JAfhnojgbV5vPYSAhWA1p0=", + }, + } + + for name, tt := range tests { + s.Run(name, func() { + // Decode the address + address, err := btcutil.DecodeAddress(tt.Address, &chaincfg.MainNetParams) + s.Require().NoError(err) + + // Decode the signature + signatureDecoded, err := base64.StdEncoding.DecodeString(tt.Signature) + s.Require().NoError(err) + + valid, err := generic.Verify(address, tt.Message, signatureDecoded, &chaincfg.MainNetParams) + s.Require().NoError(err) + s.True(valid) + }) + } +} diff --git a/internal/magic_message.go b/internal/magic_message.go index c810ad9..a36381c 100644 --- a/internal/magic_message.go +++ b/internal/magic_message.go @@ -2,6 +2,7 @@ package internal import ( "bytes" + "crypto/sha256" "github.com/btcsuite/btcd/wire" ) @@ -14,6 +15,10 @@ const varIntProtoVer uint32 = 0 // Taken from https://bitcoin.stackexchange.com/a/77325 const magicMessage = "\x18Bitcoin Signed Message:\n" +// Signed message via BIP-322 are prepended with this bip322Tag +// Taken from https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full +const bip322Tag = "BIP0322-signed-message" + // CreateMagicMessage builds a properly signed message. func CreateMagicMessage(message string) string { buffer := bytes.Buffer{} @@ -26,3 +31,12 @@ func CreateMagicMessage(message string) string { return magicMessage + buffer.String() + message } + +// CreateMagicMessageBIP322 builds a properly signed message (in BIP-322 format). +func CreateMagicMessageBIP322(message []byte) [32]byte { + tagHash := sha256.Sum256([]byte(bip322Tag)) + sum := append(tagHash[:], tagHash[:]...) // Append tagHash twice + sum = append(sum, message...) + + return sha256.Sum256(sum) +} diff --git a/internal/magic_message_test.go b/internal/magic_message_test.go index e6dcad7..571d409 100644 --- a/internal/magic_message_test.go +++ b/internal/magic_message_test.go @@ -1,6 +1,7 @@ package internal_test import ( + "encoding/hex" "testing" "github.com/stretchr/testify/require" @@ -14,3 +15,16 @@ func TestCreateMagicMessage(t *testing.T) { message := internal.CreateMagicMessage("random message") require.Equal(t, "\x18Bitcoin Signed Message:\n\x0Erandom message", message) } + +// Test vectors taken from https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#message-hashing +func TestCreateMagicMessageBIP322(t *testing.T) { + t.Parallel() + + msgHash := internal.CreateMagicMessageBIP322([]byte{}) + msgHashHex := hex.EncodeToString(msgHash[:]) + require.Equal(t, "c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1", msgHashHex) + + msgHash = internal.CreateMagicMessageBIP322([]byte("Hello World")) + msgHashHex = hex.EncodeToString(msgHash[:]) + require.Equal(t, "f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a", msgHashHex) +} diff --git a/pkg/verify.go b/pkg/verify.go index 25aca92..7a5072f 100644 --- a/pkg/verify.go +++ b/pkg/verify.go @@ -2,25 +2,16 @@ package verifier import ( "encoding/base64" - "errors" "fmt" - "reflect" "strings" - "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/samber/lo" - "github.com/bitonicnl/verify-signed-message/internal" - "github.com/bitonicnl/verify-signed-message/internal/flags" - "github.com/bitonicnl/verify-signed-message/internal/signature" + "github.com/bitonicnl/verify-signed-message/internal/bip322" + "github.com/bitonicnl/verify-signed-message/internal/generic" ) -// ExpectedSignatureLength contains the fixed signature length all signed messages are expected to have. -const ExpectedSignatureLength = 65 - // Verify will verify a SignedMessage based on the recovery flag on Bitcoin main network. func Verify(sig SignedMessage) (bool, error) { return VerifyWithChain(sig, &chaincfg.MainNetParams) @@ -44,74 +35,22 @@ func VerifyWithChain(signedMessage SignedMessage, net *chaincfg.Params) (bool, e return false, fmt.Errorf("could not decode address: %w", err) } - // Decode the signature - signatureEncoded, err := base64.StdEncoding.DecodeString(signedMessage.Signature) - if err != nil { - return false, err - } - - // Ensure signature has proper length - if len(signatureEncoded) != ExpectedSignatureLength { - return false, fmt.Errorf("wrong signature length: %d instead of 65", len(signatureEncoded)) - } - - // Ensure signature has proper recovery flag - recoveryFlag := int(signatureEncoded[0]) - if !lo.Contains[int](flags.All(), recoveryFlag) { - return false, fmt.Errorf("invalid recovery flag: %d", recoveryFlag) - } - - // Retrieve KeyID - keyID := flags.GetKeyID(recoveryFlag) - - // Should address be compressed (for checking later) - compressed := flags.ShouldBeCompressed(recoveryFlag) - - // Reset recovery flag after obtaining keyID for Trezor - if lo.Contains[int](flags.Trezor(), recoveryFlag) { - signatureEncoded[0] = byte(27 + keyID) + // Ensure the address is valid for the passed network + if !address.IsForNet(net) { + return false, fmt.Errorf("address '%s' is not valid for network '%s'", signedMessage.Address, net.Name) } - // Make the magic message - magicMessage := internal.CreateMagicMessage(signedMessage.Message) - - // Hash the message - messageHash := chainhash.DoubleHashB([]byte(magicMessage)) - - // Recover the public key from signature and message hash - publicKey, comp, err := ecdsa.RecoverCompact(signatureEncoded, messageHash) + // Decode the signature + signatureDecoded, err := base64.StdEncoding.DecodeString(signedMessage.Signature) if err != nil { - return false, fmt.Errorf("could not recover pubkey: %w", err) + return false, fmt.Errorf("could not decode signature: %w", err) } - // Ensure our initial assumption was correct, except for Trezor as they do something different - if compressed != comp && !lo.Contains[int](flags.Trezor(), recoveryFlag) { - return false, errors.New("we expected the key to be compressed, it wasn't") + // Handle generic/BIP-137 signature. For P2PKH address, assume the signature is also a legacy signature + if _, ok := address.(*btcutil.AddressPubKeyHash); ok || len(signatureDecoded) == generic.ExpectedSignatureLength { + return generic.Verify(address, signedMessage.Message, signatureDecoded, net) } - // Verify that the signature is valid - if err := signature.Verify(signatureEncoded, publicKey, messageHash); err != nil { - return false, err - } - - // Get the hash from the public key, so we can check that address matches - publicKeyHash := internal.GeneratePublicKeyHash(recoveryFlag, publicKey) - - switch address.(type) { - // Validate P2PKH - case *btcutil.AddressPubKeyHash: - return internal.ValidateP2PKH(recoveryFlag, publicKeyHash, address, net) - // Validate P2SH - case *btcutil.AddressScriptHash: - return internal.ValidateP2SH(recoveryFlag, publicKeyHash, address, net) - // Validate P2WPKH - case *btcutil.AddressWitnessPubKeyHash: - return internal.ValidateP2WPKH(recoveryFlag, publicKeyHash, address, net) - // Validate P2TR - case *btcutil.AddressTaproot: - return internal.ValidateP2TR(recoveryFlag, publicKey, address, net) - // Unsupported address - default: - return false, fmt.Errorf("unsupported address type '%s'", reflect.TypeOf(address)) - } + // Otherwise, try and verify it as BIP-322 + return bip322.Verify(address, signedMessage.Message, signatureDecoded) } diff --git a/pkg/verify_test.go b/pkg/verify_test.go index 455d445..71c4f32 100644 --- a/pkg/verify_test.go +++ b/pkg/verify_test.go @@ -1,7 +1,6 @@ package verifier_test import ( - "strings" "testing" "github.com/btcsuite/btcd/chaincfg" @@ -26,22 +25,6 @@ func (s *VerifyTestSuite) TestVerifyIncorrect() { signedMessage verifier.SignedMessage expectedError string }{ - "address - checksum mismatch": { - signedMessage: verifier.SignedMessage{ - Address: "17u1mDkgNcDwi44braeTKpvnfNnTrgvBfB", - Message: "test signature", - Signature: "IHfvfadyMsn/P0tKH6UnDnbYiZcOWWhk8xbGIWUOwTX75MR8LfEn9Mdxq5R2h1IRXKaFxbqR6SfC3sZrHBdA0Tg=", - }, - expectedError: "could not decode address: checksum mismatch", - }, - "address - different": { - signedMessage: verifier.SignedMessage{ - Address: "14wPe34dikRzK4tMYvtwMMJCEZbJ7ar35V", - Message: "test message", - Signature: "IFqUo4/sxBEFkfK8mZeeN56V13BqOc0D90oPBChF3gTqMXtNSCTN79UxC33kZ8Mi0cHy4zYCnQfCxTyLpMVXKeA=", - }, - expectedError: "generated address '1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5' does not match expected address '14wPe34dikRzK4tMYvtwMMJCEZbJ7ar35V'", - }, "address - invalid": { signedMessage: verifier.SignedMessage{ Address: "INVALID", @@ -50,14 +33,13 @@ func (s *VerifyTestSuite) TestVerifyIncorrect() { }, expectedError: "could not decode address: decoded address is of unknown format", }, - // Checksum mismatch, taken from https://github.com/scintill/php-bitcoin-signature-routines/blob/master/test/verifymessage.php#L32 - "message - different": { + "address - wrong network": { signedMessage: verifier.SignedMessage{ - Address: "14wPe34dikRzK4tMYvtwMMJCEZbJ7ar35V", - Message: "Totally different message, thus different calculated address", - Signature: "IFqUo4/sxBEFkfK8mZeeN56V13BqOc0D90oPBChF3gTqMXtNSCTN79UxC33kZ8Mi0cHy4zYCnQfCxTyLpMVXKeA=", + Address: "tb1qnzwefk7wzphlc4xeawf8p4yqtcwzdgsvukwma8", + Message: "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019.", + Signature: "AUEUpr/X2GrTv1+LUytXEAv+FDADgWkFppbx87/xz8DNEVXSunSDo1/asR9DbeAVgK3Ao4B1cAxEz3pW7wEQGmLvAQ==", }, - expectedError: "generated address '1LwzpMrpDakgZ9XPCsSuGG6ZXCE3fkNdQR' does not match expected address '14wPe34dikRzK4tMYvtwMMJCEZbJ7ar35V'", + expectedError: "address 'tb1qnzwefk7wzphlc4xeawf8p4yqtcwzdgsvukwma8' is not valid for network 'mainnet'", }, "signature - invalid": { signedMessage: verifier.SignedMessage{ @@ -65,7 +47,7 @@ func (s *VerifyTestSuite) TestVerifyIncorrect() { Message: "test message", Signature: "INVALID", }, - expectedError: "illegal base64 data at input byte 4", + expectedError: "could not decode signature: illegal base64 data at input byte 4", }, // Incorrect signature that is valid, but cannot be recovered, taken from https://github.com/scintill/php-bitcoin-signature-routines/blob/master/test/verifymessage.php#L100 "signature - invalid curve": { @@ -76,48 +58,14 @@ func (s *VerifyTestSuite) TestVerifyIncorrect() { }, expectedError: "could not recover pubkey: invalid signature: signature R + N >= P", }, - "signature - non-bitcoin": { - signedMessage: verifier.SignedMessage{ - Address: "1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5", - Message: "test message", - Signature: "zPOBbkXzwDgGVU3Gxk0noVuLq8P1pGfQUxnS0nzuxEN3qR/U/s63P81io7LV04ZxN88gVX/Qw0rzLFBR8q4IkUc=", - }, - expectedError: "invalid recovery flag: 204", - }, - "signature - too short": { + // Taken from https://github.com/luke-jr/bitcoin/blob/9ab7b8ada61a5f558c92c3eb9fd3cd3625d8cc09/src/test/util_tests.cpp#L1764 + "bip-322 - p2tr - wrong message": { signedMessage: verifier.SignedMessage{ - Address: "1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5", - Message: "test message", - Signature: "VGhpcyBpcyBub3QgdmFsaWQ=", + Address: "bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", + Message: "Hello World - This should fail", + Signature: "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==", }, - expectedError: "wrong signature length: 17 instead of 65", - }, - // Generated via https://demo.unisat.io/ and has an invalid recovery flag, which causes it to be generated uncompressed (the address is compressed). - "unisat - P2PKH": { - signedMessage: verifier.SignedMessage{ - Address: "15tbg628HntFEB7xjyVrSo3ck5jbKuGhQD", - Message: "hello world", - Signature: "G5WBoAY8ehQtP8UnS2boqjid2vYxH2/m69Il3T1SySRGVO2H1KIrTwVkPe2aU3BXyX/CYzBUaXYyWmC8vxXFIyw=", - }, - expectedError: "generated address '1NAnF6TPUieShRuhVyK5nYAGpvGwXSS7RX' does not match expected address '15tbg628HntFEB7xjyVrSo3ck5jbKuGhQD'", - }, - // Generated via https://demo.unisat.io/ and has an invalid recovery flag. - "unisat - P2WPKH-P2SH": { - signedMessage: verifier.SignedMessage{ - Address: "32ypXz5xwzGLbEnfLJWw1VUKcLbvDDVTVV", - Message: "hello world", - Signature: "HEZseoQ4aMFs8ERwwB9jm4qgoUH/sFRMTEADV9pr5EQadve7ebbsQ/LH/c7QpnDY/ygi24jlnPoZUcOT7Vo8vOw=", - }, - expectedError: "cannot use P2SH for recovery flag 'P2PKH uncompressed'", - }, - // Generated via https://demo.unisat.io/ and has an invalid recovery flag. - "unisat - P2WPKH": { - signedMessage: verifier.SignedMessage{ - Address: "bc1qzex95t5x94sq70g8u7zyc5jcn6vv27swtm5uqs", - Message: "hello world", - Signature: "HCxsLSgGi9RduaXTTzQvbpTNVR/KyWX9Rk4SU0LnhXN8T+A+8titHwMZea2PiOSQzfSu2J+og307rEw2GRZDeDE=", - }, - expectedError: "cannot use P2WPKH for recovery flag 'P2PKH uncompressed'", + expectedError: "script execution failed: ", }, } @@ -132,18 +80,16 @@ func (s *VerifyTestSuite) TestVerifyIncorrect() { func (s *VerifyTestSuite) TestVerifyWithChainTestnet() { tests := map[string]verifier.SignedMessage{ - // Generated by Tobias - "electrum legacy": { - Address: "mj6gcWsSKMyTh7QALLSH5q6HEEiN4yBuXk", - Message: "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019.", - Signature: "Hw1c1NGI5cNYTfAeDSyH/yUt67R499dZlTyNDb/Wxz8uX7I8ECS39JP8HbGpk4wD5J3PsMDdAwywBag3X8f+mw0=", - }, - // Generated by Tobias - "electrum segwit native": { + "electrum - segwit native": { Address: "tb1qr97cuq4kvq7plfetmxnl6kls46xaka78n2288z", Message: "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019.", Signature: "H/bSByRH7BW1YydfZlEx9x/nt4EAx/4A691CFlK1URbPEU5tJnTIu4emuzkgZFwC0ptvKuCnyBThnyLDCqPqT10=", }, + "sparrow - bip-322 - segwit native": { + Address: "tb1qnzwefk7wzphlc4xeawf8p4yqtcwzdgsvukwma8", + Message: "The outage comes at a time when bitcoin has been fast approaching new highs not seen since June 26, 2019.", + Signature: "AkcwRAIgLvNWZneiHQUgulpYhIFarxws7a+k/QUTlbEFgdr2bOwCIG4Za9UKDJmc7V0eoyt/rCKe1wUr3F3WqHKeoSbMaFd6ASEDElXeZo3eLtCBIF2hvhxGdJzZonHbew9M1RXYsZZX+rg=", + }, } for name, tt := range tests { @@ -157,173 +103,23 @@ func (s *VerifyTestSuite) TestVerifyWithChainTestnet() { func (s *VerifyTestSuite) TestVerify() { tests := map[string]verifier.SignedMessage{ - // Generated by Jouke - "bitcoin core": { - Address: "1CBHFokbnZVuq9fA3yjPTvSNXpdRRP7eUB", - Message: " Lorem ipsum dolor sit amet, consectetur adipiscing elit. In a turpis dignissim, tincidunt dolor quis, aliquam justo. Sed eleifend eleifend tempus. Sed blandit lectus at ullamcorper blandit. Quisque suscipit ligula lacus, tempor fringilla erat pharetra a. Curabitur pretium varius purus vel luctus. Donec fringilla velit vel risus fermentum, ac aliquam enim sollicitudin. Aliquam elementum, nunc nec malesuada fringilla, sem sem lacinia libero, id tempus nunc velit nec dui. Vestibulum gravida non tortor sit amet accumsan. Nunc semper vehicula vestibulum. Praesent at nibh dapibus, eleifend neque vitae, vehicula justo. Nam ultricies at orci vel laoreet. Morbi metus sapien, pulvinar ut dui ut, malesuada lobortis odio. Curabitur eget diam ligula. Nunc vel nisl consectetur, elementum magna et, elementum erat. Maecenas risus massa, mattis a sapien sed, molestie ullamcorper sapien. ", - Signature: "H3HQ9gwAMCee0T7M8fZTgvIYlG6pMnpP41ioDUTKjlPsOMHwrF3qmgsM+kFoWLL1u6P4ZUf3nwYacPCeBjrzFzE=", - }, - // Taken from https://github.com/btclib-org/btclib/blob/v2022.7.20/tests/ecc/test_bms.py - "bms compressed legacy": { - Address: "14dD6ygPi5WXdwwBTt1FBZK3aD8uDem1FY", - Message: "test message", - Signature: "H/iew/NhHV9V9MdUEn/LFOftaTy1ivGPKPKyMlr8OSokNC755fAxpSThNRivwTNsyY9vPUDTRYBPc2cmGd5d4y4=", - }, // Taken from https://github.com/btclib-org/btclib/blob/v2022.7.20/tests/ecc/test_bms.py - "bms compressed p2pkh": { + "generic - legacy - compressed": { Address: "1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5", Message: "test message", Signature: "IFqUo4/sxBEFkfK8mZeeN56V13BqOc0D90oPBChF3gTqMXtNSCTN79UxC33kZ8Mi0cHy4zYCnQfCxTyLpMVXKeA=", }, - // Taken from https://github.com/btclib-org/btclib/blob/v2022.7.20/tests/ecc/test_bms.py - "bms uncompressed legacy": { - Address: "1HUBHMij46Hae75JPdWjeZ5Q7KaL7EFRSD", - Message: "test message", - Signature: "G/iew/NhHV9V9MdUEn/LFOftaTy1ivGPKPKyMlr8OSokNC755fAxpSThNRivwTNsyY9vPUDTRYBPc2cmGd5d4y4=", - }, - // Taken from https://github.com/btclib-org/btclib/blob/v2022.7.20/tests/ecc/test_bms.py - "bms uncompressed p2pkh": { - Address: "19f7adDYqhHSJm2v7igFWZAqxXHj1vUa3T", - Message: "test message", - Signature: "HFqUo4/sxBEFkfK8mZeeN56V13BqOc0D90oPBChF3gTqMXtNSCTN79UxC33kZ8Mi0cHy4zYCnQfCxTyLpMVXKeA=", - }, - // Generated by Mitchell - "coinomi legacy": { - Address: "1PjSDaSiVdWW6YjwFA6FHwwfqkZdPEJUZv", - Message: "Test message!", - Signature: "IK7I33rASHdSeYDotQ9WfO4jrxgdl5ef/bTbX6Q5PNtFY9rJeAHfoZV5GpDO1K3OqoPs8ROZRXPyMNLkVOxJ+Rc=", - }, - // Generated by Mitchell - "coinomi segwit": { - Address: "39FT3L2wH56h2jmae5abPU1A7nVs6QyApV", - Message: "Test message!", - Signature: "HzpoLFjr+eUPkseb+i0Vaqj7FRm5o1+Ei/kae7XWN6nmFLmvLi7uWicerYNXjCMUf3nCnm/9UPb6SYJLI60Nh8A=", - }, - // Generated by Mitchell - "coinomi segwit native": { - Address: "bc1q0utxws6ptfdfcvaz29y4st065t5ku6vcqd364f", - Message: "Test message!", - Signature: "H+G7Fz3EVxX02kIker4HPgnP8Mlf3bT52p81hnNAahTOGJ8ANSaU0bF5RsprgTH6LXLx/PmCka48Ov7OrPw2bms=", - }, - // Generated by Mitchell - "electrum legacy": { - Address: "1CPBDkm8ER3o7r2HANcvNoVHsBYKcUHTp9", - Message: "Integer can be encoded depending on the represented value to save space. Variable length integers always precede an array/vector of a type of data that may vary in length. Longer numbers are encoded in little endian. If you're reading the Satoshi client code (BitcoinQT) it refers to this encoding as a \"CompactSize\". Modern Bitcoin Core also has the VARINT macro which implements an even more compact integer for the purpose of local storage (which is incompatible with \"CompactSize\" described here). VARINT is not a part of the protocol.", - Signature: "IHTr8YSzZ17Ut/Qaaui6BvGd42+TGwVwNYaIMUAZQTZRSqDtaTfsOcaOllPstp3IxzMlpXVOzLxNZE8r8ieffnY=", - }, - // Generated by Mitchell - "electrum legacy with spaces": { - Address: "1CPBDkm8ER3o7r2HANcvNoVHsBYKcUHTp9", - Message: " Three spaces both sides ", - Signature: "H0X6iDaOzAVqUndwAl8Ca1k8T+zbzDpfrEaXbh/FlF08bDsquG5oPnnFIfWU/8Z46jfFUwlWLfDK1iRU/DHZ6n8=", - }, - // Generated by Mitchell - "electrum segwit native": { - Address: "bc1qsdjne3y6ljndzvg9z9qrhje8k7p2m5yas704hn", - Message: "Integer can be encoded depending on the represented value to save space. Variable length integers always precede an array/vector of a type of data that may vary in length. Longer numbers are encoded in little endian. If you're reading the Satoshi client code (BitcoinQT) it refers to this encoding as a \"CompactSize\". Modern Bitcoin Core also has the VARINT macro which implements an even more compact integer for the purpose of local storage (which is incompatible with \"CompactSize\" described here). VARINT is not a part of the protocol.", - Signature: "H3TkHAXCKRfyDowCra5YRDF/Vkk2HQCel/pgEgTj9LYaWpnviSRcuYtv/CZk7NTyHsJnYP56bqbvuU3PejwLCnA=", - }, - // Generated by Mitchell - "mycelium legacy": { - Address: "13VwTBVLNpNSQVTrYpuHQVJYnk2y2Nr1ue", - Message: "Test message!", - Signature: "Hxpnr2oDFTjivFkrrp89UoMrzaAzFkkEciS3MUHCfdoEXN/KvHi9ii2Xz+FuQ6KjlZDlaPb197E8TWnhIAzbT0M=", - }, - // Generated by Mitchell - "mycelium segwit": { - Address: "325ZMWMu9vaWQeUG8Gc8MzsVKzt3Rqn8H7", - Message: "Test message!", - Signature: "IM/bkqpERGRFDGgxnceinULcqz1iRVBSUVlnDPZRKHGUQMC5t1P5wRp2/1b1+rpjFHhSS2pExB88cA750PNRlaw=", - }, - // Generated by Mitchell - "mycelium segwit native": { - Address: "bc1q58dh2fpwms37g29nw979pa65lsvjkqxq82jzvv", - Message: "Test message!", - Signature: "ILNax/LC+m3WwzIhnrieNN8DRzWTAgcVStSJmwdabUQII2fIlYUlEgnlNf4j2G4yJQoO4zFqCwaLOX4PDj1XwjA=", - }, - // Taken from https://github.com/petertodd/python-bitcoinlib/blob/master/bitcoin/tests/test_signmessage.py - "python-bitcoinlib p2pkh": { - Address: "1F26pNMrywyZJdr22jErtKcjF8R3Ttt55G", - Message: "1F26pNMrywyZJdr22jErtKcjF8R3Ttt55G", - Signature: "H85WKpqtNZDrajOnYDgUY+abh0KCAcOsAIOQwx2PftAbLEPRA7mzXA/CjXRxzz0MC225pR/hx02Vf2Ag2x33kU4=", - }, - // Dumped from (changed TestNet to MainNet): https://github.com/Samourai-Wallet/ExtLibJ/blob/develop/src/test/java/com/samourai/wallet/util/MessageSignUtilGenericTest.java - "samourai - legacy": { - Address: "1JSjyW3dZSQHv6jb6u6baXLUZnsThqmzf4", - Message: "hello foo", - Signature: "INAP+PMyI2vqIxiEKIcPOaaffspU3gAPm0YWhCxJr5iqWbQwqns9+RiXIzuU9JoNQs/MQ1BZ4O2XM23utyw3jr0=", - }, - // Dumped from (changed TestNet to MainNet): https://github.com/Samourai-Wallet/ExtLibJ/blob/develop/src/test/java/com/samourai/wallet/util/MessageSignUtilGenericTest.java - "samourai segwit native": { - Address: "bc1qnxhkjd3kcjdzqz4u0m47xj3dne907cd8yg7qdr", - Message: "hello foo", - Signature: "IJruGdQX+V6s+zvzTD3msz2l1obPchx19/bsefr+QGihcRArLSzXtkoUXA8k0NkBsIpFXGRxbG/s+eimZ+eGg70=", - }, - // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py - "trezor p2pkh": { - Address: "1JAd7XCBzGudGpJQSDSfpmJhiygtLQWaGL", - Message: "This is an example of a signed message.", - Signature: "IP2PL321I4/N0HfVIEw+aUnCYdcAJpzvwdnS3O9rlQI2MO5hf2yKz560DI7dcEycp06kr8OT9D81tOiVgyTL3Rw=", - }, - // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py - "trezor p2pkh long message": { - Address: "1JAd7XCBzGudGpJQSDSfpmJhiygtLQWaGL", - Message: strings.Repeat("VeryLongMessage!", 64), - Signature: "IApGR2zrhNBu9XhIKAJvkiyIFfV6rIN7jAEwB8qKhGDbY++Rfb6669EIscgUu+6m2x8rIkGpWOU/5xXMhrGZ2cM=", - }, - // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py - "trezor segwit native": { - Address: "bc1qannfxke2tfd4l7vhepehpvt05y83v3qsf6nfkk", - Message: "This is an example of a signed message.", - Signature: "KLVddgDZ6afipJFV3fPP2455bCB/qrgzAQ+kH7eCiIm8R89iNIp6qgkjwIMqWJ+rVB6PEutU+3EckOIwfw9msZQ=", - }, - // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py - "trezor segwit native long message": { - Address: "bc1qannfxke2tfd4l7vhepehpvt05y83v3qsf6nfkk", - Message: strings.Repeat("VeryLongMessage!", 64), - Signature: "KMb4biVeqnaMRH1jXZHaAWMaxUryI8LBgtT6NnbP7K5KGZrTOnT+BPtGw5QyrLjYPedNqQ9fARI7O32LwlK8f3E=", - }, - // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py - "trezor segwit": { - Address: "3L6TyTisPBmrDAj6RoKmDzNnj4eQi54gD2", - Message: "This is an example of a signed message.", - Signature: "I3RN5FFvrFwUCAgBVmRRajL+rZTeiXdc7H4k28JP4TMHWsCTAcTMjhl76ktkgWYdW46b8Z2Le4o4Ls21PC7gdQ0=", - }, - // Taken from https://github.com/trezor/trezor-firmware/blob/core/v2.3.4/tests/device_tests/test_msg_signmessage.py - "trezor segwit long message": { - Address: "3L6TyTisPBmrDAj6RoKmDzNnj4eQi54gD2", - Message: strings.Repeat("VeryLongMessage!", 64), - Signature: "I26t7jgGhPcHScUhQciqfDtq/YTQ5fOM+nGCPzsRBaXzTiODSlu28jn/KK2H9An0TkzmJpdUrcADiLGVB6XZOG8=", - }, - // Taken from https://github.com/bitcoinjs/bitcoinjs-message/issues/20 - "electrum P2WPKH-P2SH": { - Address: "3LbZqMMHu371r5Fjve9qNhSQzuNi7EzqUR", - Message: "test123", - Signature: "H2ehXowFWMZohHrJN+1IRdDwqN/UILqVmhIOHpeBdS4BYDCQpfDL1tTH7mNg6eeypno+Is8ApgWinkPnnz1NEq8=", - }, - // Taken from https://github.com/bitcoinjs/bitcoinjs-message/issues/20 - "trezor P2WPKH-P2SH": { - Address: "3LbZqMMHu371r5Fjve9qNhSQzuNi7EzqUR", - Message: "test123", - Signature: "I2ehXowFWMZohHrJN+1IRdDwqN/UILqVmhIOHpeBdS4BYDCQpfDL1tTH7mNg6eeypno+Is8ApgWinkPnnz1NEq8=", - }, - // Generated by Jouke using Electrum - "uncompressed p2pkh short message": { - Address: "18J72YSM9pKLvyXX1XAjFXA98zeEvxBYmw", - Message: "Test123", - Signature: "Gzhfsw0ItSrrTCChykFhPujeTyAcvVxiXwywxpHmkwFiKuUR2ETbaoFcocmcSshrtdIjfm8oXlJoTOLosZp3Yc8=", - }, - // Generated by Jouke using Electrum - "uncompressed p2pkh long message": { - Address: "18J72YSM9pKLvyXX1XAjFXA98zeEvxBYmw", - Message: " Lorem ipsum dolor sit amet, consectetur adipiscing elit. In a turpis dignissim, tincidunt dolor quis, aliquam justo. Sed eleifend eleifend tempus. Sed blandit lectus at ullamcorper blandit. Quisque suscipit ligula lacus, tempor fringilla erat pharetra a. Curabitur pretium varius purus vel luctus. Donec fringilla velit vel risus fermentum, ac aliquam enim sollicitudin. Aliquam elementum, nunc nec malesuada fringilla, sem sem lacinia libero, id tempus nunc velit nec dui. Vestibulum gravida non tortor sit amet accumsan. Nunc semper vehicula vestibulum. Praesent at nibh dapibus, eleifend neque vitae, vehicula justo. Nam ultricies at orci vel laoreet. Morbi metus sapien, pulvinar ut dui ut, malesuada lobortis odio. Curabitur eget diam ligula. Nunc vel nisl consectetur, elementum magna et, elementum erat. Maecenas risus massa, mattis a sapien sed, molestie ullamcorper sapien. ", - Signature: "HHOGSz6AUEEyVGoCUw1GqQ5qy9KvW5uO1FfqWLbwYxkQVsI+sbM0jpBQWkyjr72166yiL/LQEtW3SpVBR1gXdYY=", + // Based on the test above + "generic - legacy - compressed - untrimmed": { + Address: "1DAag8qiPLHh6hMFVu9qJQm9ro1HtwuyK5", + Message: " test message ", + Signature: "IFqUo4/sxBEFkfK8mZeeN56V13BqOc0D90oPBChF3gTqMXtNSCTN79UxC33kZ8Mi0cHy4zYCnQfCxTyLpMVXKeA=", }, - // Generated via https://demo.unisat.io/ which uses https://github.com/bitpay/bitcore - "unisat - p2tr": { - Address: "bc1pg48rw0vphy9mght5dr8s5prx92a44wpqmzk67xk8yjf5zlancj9sa3plhc", - Message: "this is a random message", - Signature: "G5Q4LobfmVKN4+CG/QF8r2mVBWE14nhbczdHWiCHaS8OcqUUzWF8A/chCyQbr95r1aG4TwUi6PZ01hDrtuuypmk=", + // BIP-322 test vector #0 - https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#user-content-Test_vectors + "bip-322 - native segwit - test vector #0": { + Address: "bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", + Message: "Hello World", + Signature: "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", }, }