diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..88e6997 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,58 @@ +name: Lint + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: 1.18 + + - name: Caching + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: go-${{ hashFiles('**/go.sum') }} + restore-keys: go- + + - name: Dependencies + run: go mod vendor + + - name: Checksum + run: go mod verify + + - name: Codestyle + uses: golangci/golangci-lint-action@v2 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: latest + + # Optional: working directory, useful for monorepos + working-directory: ./ + + # Optional: golangci-lint command line arguments. + args: --timeout 200s --build-tags=musl + + # optionally use a specific version of Go rather than the latest one + go_version: '1.18' + + - name: Golang Vulncheck + uses: Templum/govulncheck-action@v0.10.0 + with: + go-version: 1.18 + vulncheck-version: latest + fail-on-vuln: true + package: ./... diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..654b58d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: 1.18 + + - name: Caching + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: go-${{ hashFiles('**/go.sum') }} + restore-keys: go- + + - name: Dependencies + run: go mod vendor + + - name: Checksum + run: go mod verify + + - name: Test + run: go test -short -v -failfast ./... diff --git a/.gitignore b/.gitignore index 1e9ec99..0f60da4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # Folders /coverage/ /reports/ +/.example/vendor ### Go ### # Binaries for programs and plugins @@ -22,3 +23,5 @@ ### Go Patch ### /vendor/ /Godeps/ +.idea +.vscode diff --git a/README.md b/README.md index e2b97e6..473977f 100644 --- a/README.md +++ b/README.md @@ -25,35 +25,7 @@ go get -u github.com/bitonicnl/verify-signed-message ## Usage -```go -package main - -import ( - "fmt" - - "github.com/btcsuite/btcd/chaincfg" - - "github.com/bitonicnl/verify-signed-message/pkg" -) - -func main() { - // Bitcoin Mainnet - fmt.Println(verifier.Verify(verifier.SignedMessage{ - Address: "18J72YSM9pKLvyXX1XAjFXA98zeEvxBYmw", - Message: "Test123", - Signature: "Gzhfsw0ItSrrTCChykFhPujeTyAcvVxiXwywxpHmkwFiKuUR2ETbaoFcocmcSshrtdIjfm8oXlJoTOLosZp3Yc8=", - })) - - // Bitcoin Testnet3 - fmt.Println(verifier.VerifyWithChain(verifier.SignedMessage{ - 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=", - }, &chaincfg.TestNet3Params)) -} -``` - -In this example it will output `true, `, since the signature is valid and there are no errors. +For examples, checkout the [example](/.example) folder. ## Support @@ -69,10 +41,12 @@ This library tries to support as many signatures as possible. - 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 +- Taproot (P2TR) + - The verification is using the internal key, so only addresses without a tapscript are allowed. **Currently not supported:** - Pay-to-Witness-Script-Hash (P2WSH) -- Taproot ([as there is no consensus](https://github.com/trezor/trezor-firmware/issues/1943)) +- BIP-322 ## Development diff --git a/internal/validation.go b/internal/validation.go index c343ee6..09e8e87 100644 --- a/internal/validation.go +++ b/internal/validation.go @@ -4,6 +4,8 @@ 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" @@ -21,6 +23,7 @@ func ValidateP2PKH(recoveryFlag int, pubkeyHash []byte, addr btcutil.Address, ne 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() { @@ -39,6 +42,7 @@ func ValidateP2SH(recoveryFlag int, pubkeyHash []byte, addr btcutil.Address, net 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 { @@ -57,6 +61,7 @@ func ValidateP2WPKH(recoveryFlag int, pubkeyHash []byte, addr btcutil.Address, n 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() { @@ -65,3 +70,29 @@ func ValidateP2WPKH(recoveryFlag int, pubkeyHash []byte, addr btcutil.Address, n return true, nil } + +// ValidateP2TR ensures that the passed P2TR address matches the address generated from the public key hash, recovery flag and network. +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.Compressed(), recoveryFlag) { + return false, errors.New("cannot use P2TR for recovery flag 'compressed'") + } else if lo.Contains[int](flags.TrezorP2WPKHAndP2SH(), recoveryFlag) { + return false, errors.New("cannot use P2TR for recovery flag 'BIP137 (Trezor) P2WPKH-P2SH'") + } 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, 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/validation_test.go b/internal/validation_test.go index 73c2cca..b18a7bf 100644 --- a/internal/validation_test.go +++ b/internal/validation_test.go @@ -5,9 +5,11 @@ import ( "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" @@ -110,7 +112,7 @@ func (s *ValidateTestSuite) TestValidateP2SH() { want: errors.New("generated address '3Nxee1CFDqFRtUrixREpNMhsmH9TBXcY48' does not match expected address 'Invalid'"), }, { - name: "Invalid address for public key hash", + name: "Valid P2SH", args: args{recoveryFlag: 35, pubKeyHash: pubKeyHash, addr: &RandomAddress{Address: "3Nxee1CFDqFRtUrixREpNMhsmH9TBXcY48"}}, want: nil, }, @@ -151,7 +153,7 @@ func (s *ValidateTestSuite) TestValidateP2WPKH() { want: errors.New("generated address 'bc1qs4c46q43meu623fz8km84ma93rjhef7z88rg99' does not match expected address 'Invalid'"), }, { - name: "Invalid address for public key hash", + name: "Valid P2WPKH", args: args{recoveryFlag: 32, witnessProg: pubKeyHash, addr: &RandomAddress{Address: "bc1qs4c46q43meu623fz8km84ma93rjhef7z88rg99"}}, want: nil, }, @@ -165,6 +167,64 @@ func (s *ValidateTestSuite) TestValidateP2WPKH() { } } +func (s *ValidateTestSuite) TestValidateP2TR() { + // Generated via https://unisat.io/ which uses https://github.com/bitpay/bitcore + x, y := &btcec.FieldVal{}, &btcec.FieldVal{} + x.SetBytes(&[32]byte{199, 142, 160, 82, 151, 162, 66, 186, 11, 43, 16, 91, 237, 71, 91, 135, 150, 252, 234, 48, 99, 136, 19, 243, 89, 137, 196, 224, 241, 223, 158, 246}) + y.SetBytes(&[32]byte{72, 92, 183, 101, 120, 192, 238, 204, 246, 77, 101, 137, 220, 171, 222, 21, 13, 79, 125, 140, 76, 22, 138, 145, 121, 157, 206, 101, 219, 101, 217, 105}) + + pubKey := btcec.NewPublicKey(x, y) + + type args struct { + recoveryFlag int + pubKey *btcec.PublicKey + addr btcutil.Address + } + tests := []struct { + name string + args args + want error + }{ + { + name: "Invalid recovery flag - compressed", + args: args{recoveryFlag: 33, pubKey: &btcec.PublicKey{}, addr: &RandomAddress{}}, + want: errors.New("cannot use P2TR for recovery flag 'compressed'"), + }, + { + 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) P2WPKH-P2SH'"), + }, + { + 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", + args: args{recoveryFlag: 27, pubKey: pubKey, addr: &RandomAddress{Address: "Invalid"}}, + want: errors.New("generated address 'bc1pg48rw0vphy9mght5dr8s5prx92a44wpqmzk67xk8yjf5zlancj9sa3plhc' does not match expected address 'Invalid'"), + }, + { + name: "Valid P2TR", + args: args{recoveryFlag: 27, pubKey: pubKey, addr: &RandomAddress{Address: "bc1pg48rw0vphy9mght5dr8s5prx92a44wpqmzk67xk8yjf5zlancj9sa3plhc"}}, + want: nil, + }, + } + + for _, tt := range tests { + s.T().Run(tt.name, func(t *testing.T) { + _, err := internal.ValidateP2TR(tt.args.recoveryFlag, tt.args.pubKey, tt.args.addr, &chaincfg.MainNetParams) + require.Equal(t, err, tt.want) + }) + } +} + // RandomAddress implements the btcutil.Address interface and serves as a no-op to test these calls. type RandomAddress struct { Address string diff --git a/pkg/verify.go b/pkg/verify.go index f2cc651..25aca92 100644 --- a/pkg/verify.go +++ b/pkg/verify.go @@ -27,6 +27,7 @@ func Verify(sig SignedMessage) (bool, error) { } // VerifyWithChain will verify a SignedMessage based on the recovery flag on the passed network. +// Supported address types are P2PKH, P2WKH, NP2WKH (P2WPKH), P2TR. func VerifyWithChain(signedMessage SignedMessage, net *chaincfg.Params) (bool, error) { // Check if message contains spaces that can be trimmed, if so run the verification with the trimmed message // This is required because Electrum trims messages before signing @@ -96,21 +97,21 @@ func VerifyWithChain(signedMessage SignedMessage, net *chaincfg.Params) (bool, e // Get the hash from the public key, so we can check that address matches publicKeyHash := internal.GeneratePublicKeyHash(recoveryFlag, publicKey) + switch address.(type) { // Validate P2PKH - if _, ok := address.(*btcutil.AddressPubKeyHash); ok { + case *btcutil.AddressPubKeyHash: return internal.ValidateP2PKH(recoveryFlag, publicKeyHash, address, net) - } - // Validate P2SH - if _, ok := address.(*btcutil.AddressScriptHash); ok { + case *btcutil.AddressScriptHash: return internal.ValidateP2SH(recoveryFlag, publicKeyHash, address, net) - } - // Validate P2WPKH - if _, ok := address.(*btcutil.AddressWitnessPubKeyHash); ok { + 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)) } - - // Catch all, should never happen - return false, fmt.Errorf("unexpected address type '%s'", reflect.TypeOf(address)) } diff --git a/pkg/verify_test.go b/pkg/verify_test.go index f03dea0..1341891 100644 --- a/pkg/verify_test.go +++ b/pkg/verify_test.go @@ -293,6 +293,12 @@ func (s *VerifyTestSuite) TestVerify() { 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://unisat.io/ which uses https://github.com/bitpay/bitcore + "p2tr": { + Address: "bc1pg48rw0vphy9mght5dr8s5prx92a44wpqmzk67xk8yjf5zlancj9sa3plhc", + Message: "this is a random message", + Signature: "G5Q4LobfmVKN4+CG/QF8r2mVBWE14nhbczdHWiCHaS8OcqUUzWF8A/chCyQbr95r1aG4TwUi6PZ01hDrtuuypmk=", + }, } for i := range tests {