From 25b5806cf146ef5d5f5770c60f102a7b37bcf660 Mon Sep 17 00:00:00 2001 From: junderw Date: Thu, 21 May 2020 11:11:12 +0900 Subject: [PATCH] Throw errors when p2wsh or p2wpkh contain uncompressed pubkeys. This will enforce BIP143 compressed pubkey rules on an address generation level. --- src/payments/p2wpkh.js | 4 ++- src/payments/p2wsh.js | 34 ++++++++++++++++++----- test/fixtures/p2wpkh.json | 17 ++++++++++++ test/fixtures/p2wsh.json | 24 +++++++++++++++++ test/fixtures/transaction_builder.json | 4 +-- ts_src/payments/p2wpkh.ts | 4 ++- ts_src/payments/p2wsh.ts | 37 +++++++++++++++++++++----- 7 files changed, 107 insertions(+), 17 deletions(-) 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..1177f3c 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,14 @@ function stacksEqual(a, b) { return x.equals(b[i]); }); } +function chunkHasUncompressedPubkey(chunk) { + if (Buffer.isBuffer(chunk) && chunk.length === 65) { + if (ecc.isPoint(chunk)) return true; + else return false; + } else { + return false; + } +} // input: <> // witness: [redeemScriptSig ...] {redeemScript} // output: OP_0 {sha256(redeemScript)} @@ -51,6 +60,9 @@ function p2wsh(a, opts) { const _rchunks = lazy.value(() => { return bscript.decompile(a.redeem.input); }); + const _rochunks = lazy.value(() => { + return bscript.decompile(a.redeem.output); + }); let network = a.network; if (!network) { network = (a.redeem && a.redeem.network) || networks_1.bitcoin; @@ -166,14 +178,24 @@ 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 && _rochunks().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..173767d 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,15 @@ function stacksEqual(a: Buffer[], b: Buffer[]): boolean { }); } +function chunkHasUncompressedPubkey(chunk: StackElement): boolean { + if (Buffer.isBuffer(chunk) && chunk.length === 65) { + if (ecc.isPoint(chunk)) return true; + else return false; + } else { + return false; + } +} + // input: <> // witness: [redeemScriptSig ...] {redeemScript} // output: OP_0 {sha256(redeemScript)} @@ -59,6 +69,9 @@ export function p2wsh(a: Payment, opts?: PaymentOpts): Payment { const _rchunks = lazy.value(() => { return bscript.decompile(a.redeem!.input!); }) as StackFunction; + const _rochunks = lazy.value(() => { + return bscript.decompile(a.redeem!.output!); + }) as StackFunction; let network = a.network; if (!network) { @@ -187,15 +200,25 @@ 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 && _rochunks().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'); } }