diff --git a/CHANGELOG.md b/CHANGELOG.md index fecfc19..81884f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 5.1.8 +__fixed__ +- Throw errors when p2wsh or p2wpkh contain uncompressed pubkeys (#1573) + +__added__ +- Add txInputs and txOutputs for Psbt (#1561) + +__changed__ +- (Not exposed) Added BufferWriter to help ease maintenance of certain forks of this library (#1533) + # 5.1.7 __fixed__ - Fixed Transaction class Output interface typing for TypeScript (#1506) diff --git a/package-lock.json b/package-lock.json index ae2d378..b0c46ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "bitcoinjs-lib", - "version": "5.1.7", + "version": "5.1.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e94b78b..52b667c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitcoinjs-lib", - "version": "5.1.7", + "version": "5.1.8", "description": "Client-side Bitcoin JavaScript library", "main": "./src/index.js", "types": "./types/index.d.ts", diff --git a/src/payments/p2wpkh.js b/src/payments/p2wpkh.js index b32e808..0ba4a51 100644 --- a/src/payments/p2wpkh.js +++ b/src/payments/p2wpkh.js @@ -107,12 +107,14 @@ function p2wpkh(a, opts) { if (hash.length > 0 && !hash.equals(pkh)) throw new TypeError('Hash mismatch'); else hash = pkh; + if (!ecc.isPoint(a.pubkey) || a.pubkey.length !== 33) + throw new TypeError('Invalid pubkey for p2wpkh'); } if (a.witness) { if (a.witness.length !== 2) throw new TypeError('Witness is invalid'); if (!bscript.isCanonicalScriptSignature(a.witness[0])) throw new TypeError('Witness has invalid signature'); - if (!ecc.isPoint(a.witness[1])) + if (!ecc.isPoint(a.witness[1]) || a.witness[1].length !== 33) throw new TypeError('Witness has invalid pubkey'); if (a.signature && !a.signature.equals(a.witness[0])) throw new TypeError('Signature mismatch'); diff --git a/src/payments/p2wsh.js b/src/payments/p2wsh.js index 6a4aa24..f9ae90b 100644 --- a/src/payments/p2wsh.js +++ b/src/payments/p2wsh.js @@ -6,6 +6,7 @@ const bscript = require('../script'); const lazy = require('./lazy'); const typef = require('typeforce'); const OPS = bscript.OPS; +const ecc = require('tiny-secp256k1'); const bech32 = require('bech32'); const EMPTY_BUFFER = Buffer.alloc(0); function stacksEqual(a, b) { @@ -14,6 +15,18 @@ function stacksEqual(a, b) { return x.equals(b[i]); }); } +function chunkHasUncompressedPubkey(chunk) { + if ( + Buffer.isBuffer(chunk) && + chunk.length === 65 && + chunk[0] === 0x04 && + ecc.isPoint(chunk) + ) { + return true; + } else { + return false; + } +} // input: <> // witness: [redeemScriptSig ...] {redeemScript} // output: OP_0 {sha256(redeemScript)} @@ -166,14 +179,27 @@ function p2wsh(a, opts) { !stacksEqual(a.witness, a.redeem.witness) ) throw new TypeError('Witness and redeem.witness mismatch'); - } - if (a.witness) { if ( - a.redeem && - a.redeem.output && - !a.redeem.output.equals(a.witness[a.witness.length - 1]) - ) + (a.redeem.input && _rchunks().some(chunkHasUncompressedPubkey)) || + (a.redeem.output && + (bscript.decompile(a.redeem.output) || []).some( + chunkHasUncompressedPubkey, + )) + ) { + throw new TypeError( + 'redeem.input or redeem.output contains uncompressed pubkey', + ); + } + } + if (a.witness && a.witness.length > 0) { + const wScript = a.witness[a.witness.length - 1]; + if (a.redeem && a.redeem.output && !a.redeem.output.equals(wScript)) throw new TypeError('Witness and redeem.output mismatch'); + if ( + a.witness.some(chunkHasUncompressedPubkey) || + (bscript.decompile(wScript) || []).some(chunkHasUncompressedPubkey) + ) + throw new TypeError('Witness contains uncompressed pubkey'); } } return Object.assign(o, a); diff --git a/test/fixtures/p2wpkh.json b/test/fixtures/p2wpkh.json index 4d62b60..057ba81 100644 --- a/test/fixtures/p2wpkh.json +++ b/test/fixtures/p2wpkh.json @@ -113,6 +113,23 @@ "output": "OP_RESERVED ea6d525c0c955d90d3dbd29a81ef8bfb79003727" } }, + { + "exception": "Invalid pubkey for p2wpkh", + "description": "Uncompressed pubkey in pubkey", + "arguments": { + "pubkey": "049fa211b0fca342589ca381cc96520c3d0e3924832158d9f29891936cac091e80687acdca51868ee1f89a3bb36bb16f186262938e1f94c1e7692924935b9b1457" + } + }, + { + "exception": "Witness has invalid pubkey", + "description": "Uncompressed pubkey in witness", + "arguments": { + "witness": [ + "300602010002010001", + "049fa211b0fca342589ca381cc96520c3d0e3924832158d9f29891936cac091e80687acdca51868ee1f89a3bb36bb16f186262938e1f94c1e7692924935b9b1457" + ] + } + }, { "exception": "Pubkey mismatch", "options": {}, diff --git a/test/fixtures/p2wsh.json b/test/fixtures/p2wsh.json index 0350fe9..0f60112 100644 --- a/test/fixtures/p2wsh.json +++ b/test/fixtures/p2wsh.json @@ -344,6 +344,30 @@ } } }, + { + "exception": "redeem.input or redeem.output contains uncompressed pubkey", + "arguments": { + "redeem": { + "output": "049fa211b0fca342589ca381cc96520c3d0e3924832158d9f29891936cac091e80687acdca51868ee1f89a3bb36bb16f186262938e1f94c1e7692924935b9b1457 OP_CHECKSIG" + } + } + }, + { + "exception": "redeem.input or redeem.output contains uncompressed pubkey", + "arguments": { + "redeem": { + "input": "049fa211b0fca342589ca381cc96520c3d0e3924832158d9f29891936cac091e80687acdca51868ee1f89a3bb36bb16f186262938e1f94c1e7692924935b9b1457" + } + } + }, + { + "exception": "Witness contains uncompressed pubkey", + "arguments": { + "witness": [ + "049fa211b0fca342589ca381cc96520c3d0e3924832158d9f29891936cac091e80687acdca51868ee1f89a3bb36bb16f186262938e1f94c1e7692924935b9b1457" + ] + } + }, { "exception": "Invalid prefix or Network mismatch", "arguments": { diff --git a/test/fixtures/transaction_builder.json b/test/fixtures/transaction_builder.json index 07226b0..1564e6a 100644 --- a/test/fixtures/transaction_builder.json +++ b/test/fixtures/transaction_builder.json @@ -2122,7 +2122,7 @@ }, { "description": "Transaction w/ P2WSH(P2PK), signing with uncompressed public key", - "exception": "BIP143 rejects uncompressed public keys in P2WPKH or P2WSH", + "exception": "redeem.input or redeem.output contains uncompressed pubkey", "inputs": [ { "txId": "2fddebc1a7e67e04fc6b77645ae9ae10eeaa35e168606587d79b031ebca33345", @@ -2148,7 +2148,7 @@ }, { "description": "Transaction w/ P2SH(P2WSH(P2PK)), signing with uncompressed public key", - "exception": "BIP143 rejects uncompressed public keys in P2WPKH or P2WSH", + "exception": "redeem.input or redeem.output contains uncompressed pubkey", "inputs": [ { "txId": "2fddebc1a7e67e04fc6b77645ae9ae10eeaa35e168606587d79b031ebca33345", diff --git a/ts_src/payments/p2wpkh.ts b/ts_src/payments/p2wpkh.ts index fc2a458..27c61c9 100644 --- a/ts_src/payments/p2wpkh.ts +++ b/ts_src/payments/p2wpkh.ts @@ -118,13 +118,15 @@ export function p2wpkh(a: Payment, opts?: PaymentOpts): Payment { if (hash.length > 0 && !hash.equals(pkh)) throw new TypeError('Hash mismatch'); else hash = pkh; + if (!ecc.isPoint(a.pubkey) || a.pubkey.length !== 33) + throw new TypeError('Invalid pubkey for p2wpkh'); } if (a.witness) { if (a.witness.length !== 2) throw new TypeError('Witness is invalid'); if (!bscript.isCanonicalScriptSignature(a.witness[0])) throw new TypeError('Witness has invalid signature'); - if (!ecc.isPoint(a.witness[1])) + if (!ecc.isPoint(a.witness[1]) || a.witness[1].length !== 33) throw new TypeError('Witness has invalid pubkey'); if (a.signature && !a.signature.equals(a.witness[0])) diff --git a/ts_src/payments/p2wsh.ts b/ts_src/payments/p2wsh.ts index 132dde4..a72be94 100644 --- a/ts_src/payments/p2wsh.ts +++ b/ts_src/payments/p2wsh.ts @@ -1,10 +1,11 @@ import * as bcrypto from '../crypto'; import { bitcoin as BITCOIN_NETWORK } from '../networks'; import * as bscript from '../script'; -import { Payment, PaymentOpts, StackFunction } from './index'; +import { Payment, PaymentOpts, StackElement, StackFunction } from './index'; import * as lazy from './lazy'; const typef = require('typeforce'); const OPS = bscript.OPS; +const ecc = require('tiny-secp256k1'); const bech32 = require('bech32'); @@ -18,6 +19,19 @@ function stacksEqual(a: Buffer[], b: Buffer[]): boolean { }); } +function chunkHasUncompressedPubkey(chunk: StackElement): boolean { + if ( + Buffer.isBuffer(chunk) && + chunk.length === 65 && + chunk[0] === 0x04 && + ecc.isPoint(chunk) + ) { + return true; + } else { + return false; + } +} + // input: <> // witness: [redeemScriptSig ...] {redeemScript} // output: OP_0 {sha256(redeemScript)} @@ -187,15 +201,28 @@ export function p2wsh(a: Payment, opts?: PaymentOpts): Payment { !stacksEqual(a.witness, a.redeem.witness) ) throw new TypeError('Witness and redeem.witness mismatch'); + if ( + (a.redeem.input && _rchunks().some(chunkHasUncompressedPubkey)) || + (a.redeem.output && + (bscript.decompile(a.redeem.output) || []).some( + chunkHasUncompressedPubkey, + )) + ) { + throw new TypeError( + 'redeem.input or redeem.output contains uncompressed pubkey', + ); + } } - if (a.witness) { - if ( - a.redeem && - a.redeem.output && - !a.redeem.output.equals(a.witness[a.witness.length - 1]) - ) + if (a.witness && a.witness.length > 0) { + const wScript = a.witness[a.witness.length - 1]; + if (a.redeem && a.redeem.output && !a.redeem.output.equals(wScript)) throw new TypeError('Witness and redeem.output mismatch'); + if ( + a.witness.some(chunkHasUncompressedPubkey) || + (bscript.decompile(wScript) || []).some(chunkHasUncompressedPubkey) + ) + throw new TypeError('Witness contains uncompressed pubkey'); } }