Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions btcjson/chainsvrcmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -929,19 +929,23 @@ func NewSetGenerateCmd(generate bool, genProcLimit *int) *SetGenerateCmd {

// SignMessageWithPrivKeyCmd defines the signmessagewithprivkey JSON-RPC command.
type SignMessageWithPrivKeyCmd struct {
PrivKey string // base 58 Wallet Import format private key
Message string // Message to sign
PrivKey string // base 58 Wallet Import format private key
Message string // Message to sign
Address *string // optional address for BIP-322 signing
}

// NewSignMessageWithPrivKey returns a new instance which can be used to issue a
// signmessagewithprivkey JSON-RPC command.
//
// The first parameter is a private key in base 58 Wallet Import format.
// The second parameter is the message to sign.
func NewSignMessageWithPrivKey(privKey, message string) *SignMessageWithPrivKeyCmd {
// The optional third parameter is an address for BIP-322 signing. Passing nil
// will use the default legacy signing method.
func NewSignMessageWithPrivKey(privKey, message string, address *string) *SignMessageWithPrivKeyCmd {
return &SignMessageWithPrivKeyCmd{
PrivKey: privKey,
Message: message,
Address: address,
}
}

Expand Down
17 changes: 16 additions & 1 deletion btcjson/chainsvrcmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1304,14 +1304,29 @@ func TestChainSvrCmds(t *testing.T) {
return btcjson.NewCmd("signmessagewithprivkey", "5Hue", "Hey")
},
staticCmd: func() interface{} {
return btcjson.NewSignMessageWithPrivKey("5Hue", "Hey")
return btcjson.NewSignMessageWithPrivKey("5Hue", "Hey", nil)
},
marshalled: `{"jsonrpc":"1.0","method":"signmessagewithprivkey","params":["5Hue","Hey"],"id":1}`,
unmarshalled: &btcjson.SignMessageWithPrivKeyCmd{
PrivKey: "5Hue",
Message: "Hey",
},
},
{
name: "signmessagewithprivkey - with address",
newCmd: func() (interface{}, error) {
return btcjson.NewCmd("signmessagewithprivkey", "5Hue", "Hey", "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")
},
staticCmd: func() interface{} {
return btcjson.NewSignMessageWithPrivKey("5Hue", "Hey", btcjson.String("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"))
},
marshalled: `{"jsonrpc":"1.0","method":"signmessagewithprivkey","params":["5Hue","Hey","1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"],"id":1}`,
unmarshalled: &btcjson.SignMessageWithPrivKeyCmd{
PrivKey: "5Hue",
Message: "Hey",
Address: btcjson.String("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"),
},
},
{
name: "stop",
newCmd: func() (interface{}, error) {
Expand Down
123 changes: 123 additions & 0 deletions btcutil/bip322/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package bip322

import (
"errors"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
)

var (
// ErrInconclusive is returned when verification cannot be completed
// because the address uses an unknown script type (e.g., witness version > 1).
ErrInconclusive = errors.New("bip322: verification inconclusive")

// ErrUnsupportedAddressType is returned when Sign is called with an
// address type that is not supported for signing.
ErrUnsupportedAddressType = errors.New("bip322: unsupported address type for signing")

// ErrMalformedSignature is returned when the signature cannot be decoded.
ErrMalformedSignature = errors.New("bip322: malformed signature")
)

// Sign produces a BIP-322 signature for the given message using the provided
// private key, dispatching to the appropriate signing function based on the
// address type:
// - P2WPKH -> BIP-322 Simple format
// - P2TR -> BIP-322 Simple format (Schnorr key-path)
// - P2SH-P2WPKH -> BIP-322 Simple format (nested segwit)
// - P2PKH -> BIP-322 Full format
//
// Any other address type returns ErrUnsupportedAddressType.
func Sign(privKey *btcec.PrivateKey, addr btcutil.Address, message string) (string, error) {
switch addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
return SignP2WPKH(privKey, addr, message)
case *btcutil.AddressTaproot:
return SignP2TR(privKey, addr, message)
case *btcutil.AddressScriptHash:
return SignP2SHP2WPKH(privKey, addr, message)
case *btcutil.AddressPubKeyHash:
return SignP2PKH(privKey, addr, message)
default:
return "", ErrUnsupportedAddressType
}
}

// Verify verifies a BIP-322 signature against an address and message.
// The format is auto-detected from the signature bytes:
// - Valid witness stack encoding -> BIP-322 Simple format
// - Valid serialized transaction -> BIP-322 Full format
func Verify(addr btcutil.Address, message string, signature string) (bool, error) {
format, err := DetectFormat(signature)
if err != nil {
return false, ErrMalformedSignature
}

switch format {
case FormatSimple:
return verifySimple(addr, message, signature)
case FormatFull:
return verifyFull(addr, message, signature)
default:
return false, ErrMalformedSignature
}
}

// verifySimple verifies a BIP-322 Simple format signature by dispatching to
// the appropriate address-type-specific verifier.
func verifySimple(addr btcutil.Address, message string, signature string) (bool, error) {
switch addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
return VerifyP2WPKH(addr, message, signature)
case *btcutil.AddressTaproot:
return VerifyP2TR(addr, message, signature)
case *btcutil.AddressScriptHash:
return VerifyP2SHP2WPKH(addr, message, signature)
case *btcutil.AddressWitnessScriptHash:
return VerifyP2WSH(addr, message, signature)
default:
return false, ErrInconclusive
}
}

// verifyFull verifies a BIP-322 Full format signature by decoding the complete
// to_sign transaction, validating its structure, and executing the script engine
// against the expected to_spend transaction.
func verifyFull(addr btcutil.Address, message string, signature string) (bool, error) {
toSign, err := DecodeFull(signature)
if err != nil {
return false, ErrMalformedSignature
}

if err := validateToSignStructure(toSign); err != nil {
return false, nil
}

scriptPubKey, err := txscript.PayToAddrScript(addr)
if err != nil {
return false, err
}
toSpend, err := BuildToSpendTx(message, scriptPubKey)
if err != nil {
return false, err
}
if toSign.TxIn[0].PreviousOutPoint.Hash != toSpend.TxHash() {
return false, nil
}
if toSign.TxIn[0].PreviousOutPoint.Index != 0 {
return false, nil
}

prevFetcher := txscript.NewCannedPrevOutputFetcher(scriptPubKey, 0)
hashCache := txscript.NewTxSigHashes(toSign, prevFetcher)
vm, err := txscript.NewEngine(scriptPubKey, toSign, 0, bip322VerifyFlags, nil, hashCache, 0, prevFetcher)
if err != nil {
return false, err
}
if err := vm.Execute(); err != nil {
return false, nil
}
return true, nil
}
76 changes: 76 additions & 0 deletions btcutil/bip322/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package bip322

import (
"crypto/sha256"
"testing"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/stretchr/testify/require"
)

func TestBIP322UnifiedSignDispatch(t *testing.T) {
wif, p2wpkhAddr := decodeTestKey(t)

p2wpkhSig, err := Sign(wif.PrivKey, p2wpkhAddr, "Hello World")
require.NoError(t, err)
format, err := DetectFormat(p2wpkhSig)
require.NoError(t, err)
require.Equal(t, FormatSimple, format)

p2trAddr, _ := deriveTaprootAddr(t, testPrivKeyWIF)
p2trSig, err := Sign(wif.PrivKey, p2trAddr, "Hello World")
require.NoError(t, err)
format, err = DetectFormat(p2trSig)
require.NoError(t, err)
require.Equal(t, FormatSimple, format)

_, p2pkhAddr := makeP2PKHAddr(t)
p2pkhSig, err := Sign(wif.PrivKey, p2pkhAddr, "Hello World")
require.NoError(t, err)
format, err = DetectFormat(p2pkhSig)
require.NoError(t, err)
require.Equal(t, FormatFull, format)

pubKeyBytes := wif.PrivKey.PubKey().SerializeCompressed()
redeemScriptHash := btcutil.Hash160(pubKeyBytes)
p2wpkhScript := append([]byte{0x00, 0x14}, redeemScriptHash...)
scriptHash := sha256.Sum256(p2wpkhScript)
p2wshAddr, err := btcutil.NewAddressWitnessScriptHash(
scriptHash[:], &chaincfg.MainNetParams,
)
require.NoError(t, err)
_, err = Sign(wif.PrivKey, p2wshAddr, "Hello World")
require.ErrorIs(t, err, ErrUnsupportedAddressType)
}

func TestBIP322UnifiedVerifyRoundTrip(t *testing.T) {
wif, p2wpkhAddr := decodeTestKey(t)

p2wpkhSig, err := Sign(wif.PrivKey, p2wpkhAddr, "Hello World")
require.NoError(t, err)
valid, err := Verify(p2wpkhAddr, "Hello World", p2wpkhSig)
require.NoError(t, err)
require.True(t, valid)

p2trAddr, _ := deriveTaprootAddr(t, testPrivKeyWIF)
p2trSig, err := Sign(wif.PrivKey, p2trAddr, "Hello World")
require.NoError(t, err)
valid, err = Verify(p2trAddr, "Hello World", p2trSig)
require.NoError(t, err)
require.True(t, valid)

_, p2pkhAddr := makeP2PKHAddr(t)
p2pkhSig, err := Sign(wif.PrivKey, p2pkhAddr, "Hello World")
require.NoError(t, err)
valid, err = Verify(p2pkhAddr, "Hello World", p2pkhSig)
require.NoError(t, err)
require.True(t, valid)

_, p2shAddr := testP2SHP2WPKHAddress(t)
p2shSig, err := Sign(wif.PrivKey, p2shAddr, "Hello World")
require.NoError(t, err)
valid, err = Verify(p2shAddr, "Hello World", p2shSig)
require.NoError(t, err)
require.True(t, valid)
}
23 changes: 23 additions & 0 deletions btcutil/bip322/bip322.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package bip322

import (
"github.com/btcsuite/btcd/txscript"
)

// bip322VerifyFlags are the script verification flags required by BIP-322.
const bip322VerifyFlags = txscript.ScriptBip16 |
txscript.ScriptVerifyWitness |
txscript.ScriptVerifyTaproot |
txscript.ScriptVerifyCleanStack |
txscript.ScriptVerifyDERSignatures |
txscript.ScriptVerifyLowS |
txscript.ScriptVerifyMinimalData |
txscript.ScriptVerifyNullFail |
txscript.ScriptVerifyStrictEncoding |
txscript.ScriptVerifyMinimalIf |
txscript.ScriptVerifyWitnessPubKeyType |
txscript.ScriptVerifyConstScriptCode

// bip322MsgTag is the BIP-322 tagged hash message tag as defined in BIP-322.
// See https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki
var bip322MsgTag = []byte("BIP0322-signed-message")
39 changes: 39 additions & 0 deletions btcutil/bip322/bip322_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package bip322

import (
"encoding/hex"
"testing"

"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/stretchr/testify/require"
)

// TestBIP322MessageHash tests the MessageHash function against official BIP-322 test vectors.
// See https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#test-vectors
func TestBIP322MessageHash(t *testing.T) {
tests := []struct {
name string
message string
expected string
}{
{
name: "empty message",
message: "",
expected: "c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1",
},
{
name: "hello world",
message: "Hello World",
expected: "f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hash := *chainhash.TaggedHash(
bip322MsgTag, []byte(tt.message),
)
require.Equal(t, tt.expected, hex.EncodeToString(hash[:]))
})
}
}
Loading
Loading