Skip to content

Commit 3464eed

Browse files
committed
bip322: add message verification
This commit adds verification functions for the generic message signing protocol and also adds test cases for all common script types.
1 parent 44da900 commit 3464eed

10 files changed

Lines changed: 4256 additions & 86 deletions

bip322/bip322.go

Lines changed: 651 additions & 2 deletions
Large diffs are not rendered by default.

bip322/bip322_test.go

Lines changed: 236 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,278 @@
11
package bip322
22

33
import (
4-
"encoding/hex"
5-
"encoding/json"
6-
"fmt"
7-
"os"
8-
"path/filepath"
4+
"bytes"
5+
"encoding/base64"
6+
"strings"
97
"testing"
108

119
"github.com/btcsuite/btcd/address/v2"
1210
"github.com/btcsuite/btcd/chaincfg/v2"
1311
"github.com/btcsuite/btcd/txscript/v2"
12+
"github.com/btcsuite/btcd/wire/v2"
1413
"github.com/stretchr/testify/require"
1514
)
1615

17-
var (
18-
hexEncode = hex.EncodeToString
19-
)
16+
// encodeWitnessHeader writes a (count, items...) witness stack into a buffer
17+
// where each item is described by (length, data). Used for crafting edge-case
18+
// payloads below.
19+
func encodeWitnessHeader(t *testing.T, witCount uint64,
20+
items ...[]byte) []byte {
21+
22+
t.Helper()
2023

21-
// testVectors is the top-level structure of the test data file.
22-
type testVectors struct {
23-
TxHashes []txHashVector `json:"tx_hashes,omitempty"`
24-
Simple []simpleSignatureVector `json:"simple"`
24+
var buf bytes.Buffer
25+
require.NoError(t, wire.WriteVarInt(&buf, 0, witCount))
26+
for _, item := range items {
27+
require.NoError(t, wire.WriteVarInt(&buf, 0, uint64(len(item))))
28+
_, err := buf.Write(item)
29+
require.NoError(t, err)
30+
}
31+
return buf.Bytes()
2532
}
2633

27-
// txHashVector contains expected transaction hashes for a given message.
28-
type txHashVector struct {
29-
Message string `json:"message"`
30-
Address string `json:"address"`
31-
MessageHash string `json:"message_hash"`
32-
ToSpendTxHash string `json:"to_spend_tx_hash"`
33-
ToSignTxHash string `json:"to_sign_tx_hash"`
34+
// TestParseTxWitnessTooManyItems asserts that ParseTxWitness rejects witness
35+
// stacks whose declared item count exceeds maxWitnessItems, bounding the
36+
// upfront slice-header allocation.
37+
func TestParseTxWitnessTooManyItems(t *testing.T) {
38+
// Just claim maxWitnessItems+1 items — no item data follows. If the
39+
// parser allocated first, it would commit ~24 MB from a 5-byte input.
40+
var buf bytes.Buffer
41+
require.NoError(t, wire.WriteVarInt(&buf, 0, uint64(maxWitnessItems+1)))
42+
43+
_, err := ParseTxWitness(buf.Bytes())
44+
require.Error(t, err)
45+
require.Contains(t, err.Error(), "too many witness items")
3446
}
3547

36-
// simpleSignatureVector contains BIP-322 signature data for a given "simple"
37-
// variant test case.
38-
type simpleSignatureVector struct {
39-
Message string `json:"message"`
40-
PrivateKeys []string `json:"private_keys"`
41-
Address string `json:"address"`
42-
Type string `json:"type"`
43-
WitnessScript string `json:"witness_script"`
44-
Bip322Signatures []string `json:"bip322_signatures"`
48+
// TestParseTxWitnessItemTooLarge asserts that ParseTxWitness rejects a witness
49+
// stack containing a single item whose declared length exceeds
50+
// maxWitnessItems+1.
51+
func TestParseTxWitnessItemTooLarge(t *testing.T) {
52+
// 1 item, declared length = maxWitnessItems + 1, no body.
53+
var buf bytes.Buffer
54+
require.NoError(t, wire.WriteVarInt(&buf, 0, 1))
55+
require.NoError(
56+
t, wire.WriteVarInt(&buf, 0, uint64(maxWitnessItems+1)),
57+
)
58+
59+
_, err := ParseTxWitness(buf.Bytes())
60+
require.Error(t, err)
61+
require.Contains(t, err.Error(), "witness item too large")
4562
}
4663

47-
// fullSignatureVector contains BIP-322 signature data for a given "full"
48-
// variant test case.
49-
type fullSignatureVector struct {
50-
Message string `json:"message"`
51-
PrivateKeys []string `json:"private_keys"`
52-
Address string `json:"address"`
53-
Type string `json:"type"`
54-
WitnessScript string `json:"witness_script"`
55-
TxVersion int32 `json:"tx_version"`
56-
LockTime uint32 `json:"lock_time"`
57-
Sequence uint32 `json:"sequence"`
58-
Bip322Signatures []string `json:"bip322_signatures"`
64+
// TestParseTxWitnessItemExceedsRemaining asserts that ParseTxWitness rejects
65+
// a witness item whose declared length is larger than the remaining input,
66+
// BEFORE committing a potentially large allocation.
67+
func TestParseTxWitnessItemExceedsRemaining(t *testing.T) {
68+
// 1 item, declared length = 100_000, but only 1 body byte available.
69+
var buf bytes.Buffer
70+
require.NoError(t, wire.WriteVarInt(&buf, 0, 1))
71+
require.NoError(t, wire.WriteVarInt(&buf, 0, 100_000))
72+
buf.WriteByte(0xaa)
73+
74+
_, err := ParseTxWitness(buf.Bytes())
75+
require.Error(t, err)
76+
require.Contains(t, err.Error(), "exceeds remaining input")
5977
}
6078

61-
// loadTestVectors reads and parses a test data file.
62-
func loadTestVectors(t *testing.T, fileName string) *testVectors {
63-
t.Helper()
79+
// TestParseTxWitnessRawTooLarge asserts that ParseTxWitness rejects raw
80+
// inputs larger than maxSignatureBytes up front.
81+
func TestParseTxWitnessRawTooLarge(t *testing.T) {
82+
raw := make([]byte, maxWitnessItems+1)
83+
_, err := ParseTxWitness(raw)
84+
require.ErrorIs(t, err, errSignatureTooLarge)
85+
}
6486

65-
data, err := os.ReadFile(filepath.Join("testdata", fileName))
66-
require.NoError(t, err)
87+
// TestParseTxWitnessValidSmallStack asserts that ordinary, well-formed witness
88+
// stacks inside the new bounds still parse.
89+
func TestParseTxWitnessValidSmallStack(t *testing.T) {
90+
sig := bytes.Repeat([]byte{0xaa}, 72)
91+
pub := bytes.Repeat([]byte{0xbb}, 33)
92+
93+
raw := encodeWitnessHeader(t, 2, sig, pub)
6794

68-
var vectors testVectors
69-
err = json.Unmarshal(data, &vectors)
95+
stack, err := ParseTxWitness(raw)
7096
require.NoError(t, err)
97+
require.Len(t, stack, 2)
98+
require.Equal(t, sig, stack[0])
99+
require.Equal(t, pub, stack[1])
100+
}
71101

72-
return &vectors
102+
// TestParseTxTooLarge asserts that ParseTx rejects raw inputs larger than
103+
// maxSignatureBytes.
104+
func TestParseTxTooLarge(t *testing.T) {
105+
raw := make([]byte, maxWitnessItems+1)
106+
_, err := ParseTx(raw)
107+
require.ErrorIs(t, err, errSignatureTooLarge)
73108
}
74109

75-
// testName returns a human-readable sub-test name for a given message.
76-
func testName(message string) string {
77-
if message == "" {
78-
return "msg=<empty>"
79-
}
110+
// TestVerifyMessageNilAddress asserts that VerifyMessage returns a clean
111+
// error (not a panic) when the caller passes a nil address.
112+
func TestVerifyMessageNilAddress(t *testing.T) {
113+
require.NotPanics(t, func() {
114+
valid, _, err := VerifyMessage("msg", nil, "AA==")
115+
require.False(t, valid)
116+
require.ErrorIs(t, err, errNilAddress)
117+
})
118+
}
119+
120+
// TestVerifyMessageSignatureTooLarge asserts that VerifyMessage rejects
121+
// signatures whose base64-decoded size exceeds maxSignatureBytes.
122+
func TestVerifyMessageSignatureTooLarge(t *testing.T) {
123+
addr, err := address.DecodeAddress(
124+
"bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
125+
&chaincfg.MainNetParams,
126+
)
127+
require.NoError(t, err)
80128

81-
return fmt.Sprintf("msg=%s", message)
129+
huge := make([]byte, maxWitnessItems+1)
130+
sig := base64.StdEncoding.EncodeToString(huge)
131+
132+
valid, _, err := VerifyMessage("msg", addr, sig)
133+
require.False(t, valid)
134+
require.ErrorIs(t, err, errSignatureTooLarge)
82135
}
83136

84-
// TestTxHashes tests the tx_hashes test vectors as mentioned in BIP-322:
85-
// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki
86-
func TestTxHashes(t *testing.T) {
87-
vectors := loadTestVectors(t, "basic-test-vectors.json")
137+
// bareMultisigScript builds OP_2 <pub1> <pub2> OP_2 OP_CHECKMULTISIG — a
138+
// script that is neither P2PK, P2PKH, P2SH, nor any native SegWit type.
139+
// Under the old exclusion-list gate this was silently accepted; under the
140+
// inclusion-list gate it must be rejected.
141+
func bareMultisigScript(t *testing.T) []byte {
142+
t.Helper()
88143

89-
for _, tc := range vectors.TxHashes {
90-
t.Run(testName(tc.Message), func(t *testing.T) {
91-
addr, err := address.DecodeAddress(
92-
tc.Address, &chaincfg.MainNetParams,
93-
)
94-
require.NoError(t, err)
144+
// Two arbitrary 33-byte compressed pubkeys (bytes are not valid
145+
// curve points, but txscript.PayToAddrScript isn't involved here —
146+
// we just need the script shape).
147+
pub1 := bytes.Repeat([]byte{0x02}, 33)
148+
pub2 := bytes.Repeat([]byte{0x03}, 33)
95149

96-
scriptPubKey, err := txscript.PayToAddrScript(addr)
97-
require.NoError(t, err)
150+
b := txscript.NewScriptBuilder()
151+
b.AddOp(txscript.OP_2)
152+
b.AddData(pub1)
153+
b.AddData(pub2)
154+
b.AddOp(txscript.OP_2)
155+
b.AddOp(txscript.OP_CHECKMULTISIG)
98156

99-
toSpendTx := buildToSpendTx(
100-
[]byte(tc.Message), scriptPubKey,
101-
)
157+
script, err := b.Script()
158+
require.NoError(t, err)
159+
return script
160+
}
102161

103-
// The message hash must be set as the OP_PUSH of the
104-
// first input's scriptSig.
105-
msgHash := toSpendTx.TxIn[0].SignatureScript[2:]
106-
require.Equal(t, tc.MessageHash, hexEncode(msgHash))
162+
// TestBuildToSignPacketSimpleRejectsNonNativeSegWit asserts that
163+
// BuildToSignPacketSimple rejects every non-native-SegWit script type,
164+
// including ones that slipped through the previous exclusion-list gate.
165+
func TestBuildToSignPacketSimpleRejectsNonNativeSegWit(t *testing.T) {
166+
cases := []struct {
167+
name string
168+
pkScript []byte
169+
}{
170+
{
171+
name: "empty pkScript",
172+
pkScript: nil,
173+
},
174+
{
175+
name: "OP_RETURN",
176+
pkScript: []byte{txscript.OP_RETURN},
177+
},
178+
{
179+
name: "bare multisig",
180+
pkScript: bareMultisigScript(t),
181+
},
182+
{
183+
// A plausible-looking but non-existent future witness
184+
// version. We want the gate to fail *closed* on
185+
// unknown versions.
186+
name: "future witness version (v2)",
187+
pkScript: append(
188+
[]byte{txscript.OP_2, 0x20},
189+
bytes.Repeat([]byte{0xaa}, 32)...,
190+
),
191+
},
192+
{
193+
// Random garbage bytes.
194+
name: "random bytes",
195+
pkScript: bytes.Repeat([]byte{0x42}, 20),
196+
},
197+
}
107198

108-
require.Equal(
109-
t, tc.ToSpendTxHash,
110-
toSpendTx.TxHash().String(),
199+
for _, tc := range cases {
200+
t.Run(tc.name, func(t *testing.T) {
201+
_, err := BuildToSignPacketSimple(
202+
[]byte("msg"), tc.pkScript,
111203
)
204+
require.ErrorIs(t, err, errSimpleSegWitOnly)
205+
})
206+
}
207+
}
112208

113-
toSignTx, err := BuildToSignPacketSimple(
114-
[]byte(tc.Message), scriptPubKey,
209+
// TestVerifyMessageSimpleRejectsNonNativeSegWit mirrors the BuildToSignPacket
210+
// test for the verification path.
211+
func TestVerifyMessageSimpleRejectsNonNativeSegWit(t *testing.T) {
212+
scripts := map[string][]byte{
213+
"OP_RETURN": {txscript.OP_RETURN},
214+
"bare multisig": bareMultisigScript(t),
215+
"random bytes": bytes.Repeat([]byte{0x42}, 20),
216+
}
217+
218+
for name, pkScript := range scripts {
219+
t.Run(name, func(t *testing.T) {
220+
valid, _, err := VerifyMessageSimple(
221+
[]byte("msg"), pkScript, wire.TxWitness{},
115222
)
116-
require.NoError(t, err)
223+
require.False(t, valid)
224+
require.ErrorIs(t, err, errSimpleSegWitOnly)
225+
})
226+
}
227+
}
117228

118-
require.Equal(
119-
t, tc.ToSignTxHash,
120-
toSignTx.UnsignedTx.TxHash().String(),
229+
// TestBuildToSignPacketSimpleAcceptsNativeSegWit asserts that the inclusion
230+
// list still accepts all three native SegWit script types.
231+
func TestBuildToSignPacketSimpleAcceptsNativeSegWit(t *testing.T) {
232+
// Minimal but well-formed pkScripts for each native SegWit type.
233+
p2wpkh := append(
234+
[]byte{txscript.OP_0, 0x14},
235+
bytes.Repeat([]byte{0xaa}, 20)...,
236+
)
237+
p2wsh := append(
238+
[]byte{txscript.OP_0, 0x20},
239+
bytes.Repeat([]byte{0xbb}, 32)...,
240+
)
241+
p2tr := append(
242+
[]byte{txscript.OP_1, 0x20},
243+
bytes.Repeat([]byte{0xcc}, 32)...,
244+
)
245+
246+
cases := map[string][]byte{
247+
"p2wpkh": p2wpkh,
248+
"p2wsh": p2wsh,
249+
"p2tr": p2tr,
250+
}
251+
for name, pkScript := range cases {
252+
t.Run(name, func(t *testing.T) {
253+
packet, err := BuildToSignPacketSimple(
254+
[]byte("msg"), pkScript,
121255
)
256+
require.NoError(t, err)
257+
require.NotNil(t, packet)
122258
})
123259
}
124260
}
261+
262+
// TestVerifyMessageDecodesBase64Error asserts the base64 decode path still
263+
// returns a clean error (not a panic or bool=true).
264+
func TestVerifyMessageDecodesBase64Error(t *testing.T) {
265+
addr, err := address.DecodeAddress(
266+
"bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l",
267+
&chaincfg.MainNetParams,
268+
)
269+
require.NoError(t, err)
270+
271+
valid, _, err := VerifyMessage("msg", addr, "not valid base64!!!")
272+
require.False(t, valid)
273+
require.Error(t, err)
274+
require.True(
275+
t, strings.Contains(err.Error(), "base64"),
276+
"expected base64 error, got %q", err.Error(),
277+
)
278+
}

bip322/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.25.0
55
require (
66
github.com/btcsuite/btcd/address/v2 v2.0.0
77
github.com/btcsuite/btcd/btcec/v2 v2.5.0
8+
github.com/btcsuite/btcd/btcutil/v2 v2.0.0
89
github.com/btcsuite/btcd/chaincfg/v2 v2.0.0
910
github.com/btcsuite/btcd/chainhash/v2 v2.0.0
1011
github.com/btcsuite/btcd/psbt/v2 v2.0.0
@@ -14,7 +15,6 @@ require (
1415
)
1516

1617
require (
17-
github.com/btcsuite/btcd/btcutil/v2 v2.0.0 // indirect
1818
github.com/btcsuite/btclog v1.0.0 // indirect
1919
github.com/davecgh/go-spew v1.1.1 // indirect
2020
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect

bip322/signing.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,13 @@ func signInputWitness(packet *psbt.Packet, idx int, script []byte,
113113
if txIn.PreviousOutPoint.Index >= uint32(len(prevTx.TxOut)) {
114114
return fmt.Errorf("input %d has no UTXO", idx)
115115
}
116-
return fmt.Errorf("input %d has no witness UTXO", idx)
116+
117+
if prevTx.TxHash() != txIn.PreviousOutPoint.Hash {
118+
return fmt.Errorf("non witness utxo does not match " +
119+
"input prevout")
120+
}
121+
122+
utxo = prevTx.TxOut[txIn.PreviousOutPoint.Index]
117123
}
118124

119125
prevOutFetcher := PsbtPrevOutputFetcher(packet)

0 commit comments

Comments
 (0)