|
| 1 | +// Package bip322 implements generic message signing. For more details on |
| 2 | +// BIP-322 see: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki |
| 3 | +package bip322 |
| 4 | + |
| 5 | +import ( |
| 6 | + "bytes" |
| 7 | + "encoding/base64" |
| 8 | + "errors" |
| 9 | + "fmt" |
| 10 | + |
| 11 | + "github.com/btcsuite/btcd/btcutil/psbt" |
| 12 | + "github.com/btcsuite/btcd/chaincfg/chainhash" |
| 13 | + "github.com/btcsuite/btcd/txscript" |
| 14 | + "github.com/btcsuite/btcd/wire" |
| 15 | +) |
| 16 | + |
| 17 | +const ( |
| 18 | + // TagBIP0322SignedMsg is the BIP-0322 tag for a signed message. It is |
| 19 | + // declared as an untyped string constant so it cannot be mutated at |
| 20 | + // runtime (which would silently break all sign/verify operations in |
| 21 | + // the same process). |
| 22 | + TagBIP0322SignedMsg = "BIP0322-signed-message" |
| 23 | + |
| 24 | + // PrefixSimple is the signature prefix for the "simple" variant of |
| 25 | + // BIP-322. |
| 26 | + PrefixSimple = "smp" |
| 27 | + |
| 28 | + // PrefixFull is the signature prefix for the "full" variant of BIP-322. |
| 29 | + PrefixFull = "ful" |
| 30 | + |
| 31 | + // PrefixProofOfFunds is the signature prefix for the "proof of funds" |
| 32 | + // variant of BIP-322. |
| 33 | + PrefixProofOfFunds = "pof" |
| 34 | +) |
| 35 | + |
| 36 | +var ( |
| 37 | + // errSimpleSegWitOnly is returned when a non-native SegWit output is |
| 38 | + // attempted to be signed with the "simple" variant of BIP-322. |
| 39 | + errSimpleSegWitOnly = errors.New( |
| 40 | + "only native SegWit outputs (P2WPKH, P2WSH, P2TR) are " + |
| 41 | + "supported for the simple variant", |
| 42 | + ) |
| 43 | + |
| 44 | + // b64Encode is a shortcut for the standard base64 encoding function. |
| 45 | + b64Encode = base64.StdEncoding.EncodeToString |
| 46 | + |
| 47 | + // b64Decode is a shortcut for the standard base64 decoding function. |
| 48 | + b64Decode = base64.StdEncoding.DecodeString |
| 49 | +) |
| 50 | + |
| 51 | +// buildToSpendTx constructs a transaction to spend an output using the |
| 52 | +// specified message and output script. It computes the message hash, |
| 53 | +// constructs the scriptSig, and creates the to_spend transaction, according to |
| 54 | +// BIP-322. |
| 55 | +func buildToSpendTx(message, outPkScript []byte) *wire.MsgTx { |
| 56 | + // Compute the message tagged hash: |
| 57 | + // SHA256(SHA256(tag) || SHA256(tag) || message). |
| 58 | + messageHash := *chainhash.TaggedHash( |
| 59 | + []byte(TagBIP0322SignedMsg), message, |
| 60 | + ) |
| 61 | + |
| 62 | + // Construct the scriptSig - OP_0 PUSH32[ message_hash ]. |
| 63 | + scriptSig := append([]byte{0x00, 0x20}, messageHash[:]...) |
| 64 | + |
| 65 | + // Create to_spend transaction in accordance to BIP-322: |
| 66 | + // https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full |
| 67 | + return &wire.MsgTx{ |
| 68 | + Version: 0, |
| 69 | + LockTime: 0, |
| 70 | + TxIn: []*wire.TxIn{{ |
| 71 | + PreviousOutPoint: wire.OutPoint{ |
| 72 | + Index: 0xFFFFFFFF, |
| 73 | + }, |
| 74 | + Sequence: 0, |
| 75 | + SignatureScript: scriptSig, |
| 76 | + Witness: wire.TxWitness{}, |
| 77 | + }}, |
| 78 | + TxOut: []*wire.TxOut{{ |
| 79 | + Value: 0, |
| 80 | + PkScript: outPkScript, |
| 81 | + }}, |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +// isNativeSegWitPkScript returns true iff pkScript is one of the native SegWit |
| 86 | +// script types (P2WPKH, P2WSH, P2TR) supported by the BIP-322 "simple" variant. |
| 87 | +func isNativeSegWitPkScript(pkScript []byte) bool { |
| 88 | + return txscript.IsPayToWitnessPubKeyHash(pkScript) || |
| 89 | + txscript.IsPayToWitnessScriptHash(pkScript) || |
| 90 | + txscript.IsPayToTaproot(pkScript) |
| 91 | +} |
| 92 | + |
| 93 | +// BuildToSignPacketSimple constructs a transaction template PSBT packet to |
| 94 | +// prepare for signing, using the message and spend pkScript for the "simple" |
| 95 | +// variant of the BIP-322 specification. It creates the to_sign transaction |
| 96 | +// template, according to BIP-322. This can only be used for native SegWit |
| 97 | +// outputs (P2WPKH, P2WSH, P2TR), and the spend pkScript is validated |
| 98 | +// accordingly. |
| 99 | +func BuildToSignPacketSimple(message, pkScript []byte) (*psbt.Packet, error) { |
| 100 | + // Enforce an inclusion list: only native SegWit outputs are valid for |
| 101 | + // the simple variant. Any other script type (legacy P2PKH/P2SH, bare |
| 102 | + // multisig, OP_RETURN, unknown future witness versions, etc.) must use |
| 103 | + // the full variant. |
| 104 | + if !isNativeSegWitPkScript(pkScript) { |
| 105 | + return nil, errSimpleSegWitOnly |
| 106 | + } |
| 107 | + |
| 108 | + return BuildToSignPacketFull(message, pkScript, 0, 0, 0), nil |
| 109 | +} |
| 110 | + |
| 111 | +// BuildToSignPacketFull constructs a transaction template PSBT packet to |
| 112 | +// prepare for signing, using the message, spend pkScript, and tx parameters for |
| 113 | +// the "full" variant of the BIP-322 specification. It creates the to_sign |
| 114 | +// transaction template, according to BIP-322. |
| 115 | +func BuildToSignPacketFull(message, spendPkScript []byte, |
| 116 | + txVersion int32, lockTime, sequence uint32) *psbt.Packet { |
| 117 | + |
| 118 | + msg := string(message) |
| 119 | + spendTx := buildToSpendTx(message, spendPkScript) |
| 120 | + |
| 121 | + // Create to_sign transaction in accordance to BIP-322: |
| 122 | + // https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full |
| 123 | + packet := &psbt.Packet{ |
| 124 | + UnsignedTx: &wire.MsgTx{ |
| 125 | + Version: txVersion, |
| 126 | + LockTime: lockTime, |
| 127 | + TxIn: []*wire.TxIn{{ |
| 128 | + PreviousOutPoint: wire.OutPoint{ |
| 129 | + Hash: spendTx.TxHash(), |
| 130 | + Index: 0, |
| 131 | + }, |
| 132 | + Sequence: sequence, |
| 133 | + }}, |
| 134 | + TxOut: []*wire.TxOut{{ |
| 135 | + Value: 0, |
| 136 | + PkScript: []byte{txscript.OP_RETURN}, |
| 137 | + }}, |
| 138 | + }, |
| 139 | + Inputs: []psbt.PInput{{ |
| 140 | + WitnessUtxo: &wire.TxOut{ |
| 141 | + Value: 0, |
| 142 | + PkScript: spendPkScript, |
| 143 | + }, |
| 144 | + NonWitnessUtxo: spendTx, |
| 145 | + }}, |
| 146 | + Outputs: []psbt.POutput{{}}, |
| 147 | + GenericSignedMessage: &msg, |
| 148 | + } |
| 149 | + |
| 150 | + // Legacy scripts can't have a witness UTXO, otherwise the PSBT |
| 151 | + // extraction will fail. |
| 152 | + if txscript.IsPayToPubKey(spendPkScript) || |
| 153 | + txscript.IsPayToPubKeyHash(spendPkScript) { |
| 154 | + |
| 155 | + packet.Inputs[0].WitnessUtxo = nil |
| 156 | + } |
| 157 | + |
| 158 | + return packet |
| 159 | +} |
| 160 | + |
| 161 | +// SerializeTxWitness returns the wire witness stack as raw bytes. |
| 162 | +func SerializeTxWitness(txWitness wire.TxWitness) ([]byte, error) { |
| 163 | + var witnessBytes bytes.Buffer |
| 164 | + err := psbt.WriteTxWitness(&witnessBytes, txWitness) |
| 165 | + if err != nil { |
| 166 | + return nil, fmt.Errorf("error serializing witness: %w", err) |
| 167 | + } |
| 168 | + |
| 169 | + return witnessBytes.Bytes(), nil |
| 170 | +} |
| 171 | + |
| 172 | +// SerializeSignature serializes the signature of a finalized PSBT packet of a |
| 173 | +// BIP-322 to_sign transaction. According to the rules described in the BIP, |
| 174 | +// this writes the signature as one of the three formats: |
| 175 | +// 1. Simple (smp): Version and LockTime are 0, only one input with Sequence 0. |
| 176 | +// 2. Full (ful): Version or LockTime or Sequence are non-zero, single input. |
| 177 | +// 3. Proof of Funds (pof): Version or LockTime or Sequence are non-zero, |
| 178 | +// multiple inputs. |
| 179 | +func SerializeSignature(finalizedPacket *psbt.Packet) (string, error) { |
| 180 | + if finalizedPacket == nil { |
| 181 | + return "", errors.New("nil packet") |
| 182 | + } |
| 183 | + |
| 184 | + // Prevent us from panicking on either if the length doesn't match. |
| 185 | + if len(finalizedPacket.Inputs) != len(finalizedPacket.UnsignedTx.TxIn) { |
| 186 | + return "", errors.New("input and txin length mismatch") |
| 187 | + } |
| 188 | + |
| 189 | + // At this point at least one input must be provided. |
| 190 | + if len(finalizedPacket.Inputs) == 0 { |
| 191 | + return "", errors.New("missing inputs") |
| 192 | + } |
| 193 | + |
| 194 | + // There is no exported IsFinalized function. But calling |
| 195 | + // MaybeFinalizeAll on an already finalized packet should not produce an |
| 196 | + // error if it's already finalized. |
| 197 | + if err := psbt.MaybeFinalizeAll(finalizedPacket); err != nil { |
| 198 | + return "", fmt.Errorf("packet must be finalizable: %w", err) |
| 199 | + } |
| 200 | + |
| 201 | + tx := finalizedPacket.UnsignedTx |
| 202 | + utxo := finalizedPacket.Inputs[0].WitnessUtxo |
| 203 | + if utxo == nil { |
| 204 | + if finalizedPacket.Inputs[0].NonWitnessUtxo == nil { |
| 205 | + return "", errors.New("missing utxo") |
| 206 | + } |
| 207 | + |
| 208 | + // The to_spend transaction must have exactly one output to be |
| 209 | + // a valid BIP-322 previous transaction to the to_sign's first |
| 210 | + // input. |
| 211 | + prevTx := finalizedPacket.Inputs[0].NonWitnessUtxo |
| 212 | + if len(prevTx.TxOut) != 1 { |
| 213 | + return "", errors.New("invalid non witness UTXO") |
| 214 | + } |
| 215 | + |
| 216 | + utxo = prevTx.TxOut[0] |
| 217 | + } |
| 218 | + |
| 219 | + // Detect the variant of the signature. |
| 220 | + switch { |
| 221 | + // Proof of Fund (pof) variant has multiple inputs. |
| 222 | + case len(finalizedPacket.Inputs) > 1: |
| 223 | + content, err := finalizedPacket.B64Encode() |
| 224 | + if err != nil { |
| 225 | + return "", fmt.Errorf("error encoding packet: %w", err) |
| 226 | + } |
| 227 | + |
| 228 | + return PrefixProofOfFunds + content, nil |
| 229 | + |
| 230 | + // Full (ful) variant has non-zero version, locktime, or sequence or a |
| 231 | + // non-native SegWit input. |
| 232 | + case tx.Version != 0 || tx.LockTime != 0 || tx.TxIn[0].Sequence != 0 || |
| 233 | + !isNativeSegWitPkScript(utxo.PkScript): |
| 234 | + |
| 235 | + signedTx, err := psbt.Extract(finalizedPacket) |
| 236 | + if err != nil { |
| 237 | + return "", fmt.Errorf("error extracting packet: %w", |
| 238 | + err) |
| 239 | + } |
| 240 | + |
| 241 | + var signedTxBytes bytes.Buffer |
| 242 | + err = signedTx.Serialize(&signedTxBytes) |
| 243 | + if err != nil { |
| 244 | + return "", fmt.Errorf("error serializing signed tx: "+ |
| 245 | + "%w", err) |
| 246 | + } |
| 247 | + |
| 248 | + content := b64Encode(signedTxBytes.Bytes()) |
| 249 | + return PrefixFull + content, nil |
| 250 | + |
| 251 | + // The simple (smp) variant is used if the above cases don't match. |
| 252 | + default: |
| 253 | + signedTx, err := psbt.Extract(finalizedPacket) |
| 254 | + if err != nil { |
| 255 | + return "", fmt.Errorf("error extracting packet: %w", |
| 256 | + err) |
| 257 | + } |
| 258 | + |
| 259 | + witnessBytes, err := SerializeTxWitness( |
| 260 | + signedTx.TxIn[0].Witness, |
| 261 | + ) |
| 262 | + if err != nil { |
| 263 | + return "", fmt.Errorf("error serializing witness: %w", |
| 264 | + err) |
| 265 | + } |
| 266 | + |
| 267 | + content := b64Encode(witnessBytes) |
| 268 | + return PrefixSimple + content, nil |
| 269 | + } |
| 270 | +} |
0 commit comments