diff --git a/Dockerfile b/Dockerfile index 159026d102..3d73a6b1b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,8 +45,6 @@ COPY --from=builder /tmp/bitgo/modules/sdk-opensslbytes /var/modules/sdk-openssl COPY --from=builder /tmp/bitgo/modules/secp256k1 /var/modules/secp256k1/ COPY --from=builder /tmp/bitgo/modules/sjcl /var/modules/sjcl/ COPY --from=builder /tmp/bitgo/modules/statics /var/modules/statics/ -COPY --from=builder /tmp/bitgo/modules/utxo-core /var/modules/utxo-core/ -COPY --from=builder /tmp/bitgo/modules/unspents /var/modules/unspents/ COPY --from=builder /tmp/bitgo/modules/utxo-lib /var/modules/utxo-lib/ COPY --from=builder /tmp/bitgo/modules/blake2b /var/modules/blake2b/ COPY --from=builder /tmp/bitgo/modules/blake2b-wasm /var/modules/blake2b-wasm/ @@ -55,6 +53,8 @@ COPY --from=builder /tmp/bitgo/modules/abstract-utxo /var/modules/abstract-utxo/ COPY --from=builder /tmp/bitgo/modules/blockapis /var/modules/blockapis/ COPY --from=builder /tmp/bitgo/modules/sdk-api /var/modules/sdk-api/ COPY --from=builder /tmp/bitgo/modules/sdk-hmac /var/modules/sdk-hmac/ +COPY --from=builder /tmp/bitgo/modules/unspents /var/modules/unspents/ +COPY --from=builder /tmp/bitgo/modules/utxo-core /var/modules/utxo-core/ COPY --from=builder /tmp/bitgo/modules/utxo-ord /var/modules/utxo-ord/ COPY --from=builder /tmp/bitgo/modules/account-lib /var/modules/account-lib/ COPY --from=builder /tmp/bitgo/modules/sdk-coin-ada /var/modules/sdk-coin-ada/ @@ -143,8 +143,6 @@ cd /var/modules/sdk-opensslbytes && yarn link && \ cd /var/modules/secp256k1 && yarn link && \ cd /var/modules/sjcl && yarn link && \ cd /var/modules/statics && yarn link && \ -cd /var/modules/utxo-core && yarn link && \ -cd /var/modules/unspents && yarn link && \ cd /var/modules/utxo-lib && yarn link && \ cd /var/modules/blake2b && yarn link && \ cd /var/modules/blake2b-wasm && yarn link && \ @@ -153,6 +151,8 @@ cd /var/modules/abstract-utxo && yarn link && \ cd /var/modules/blockapis && yarn link && \ cd /var/modules/sdk-api && yarn link && \ cd /var/modules/sdk-hmac && yarn link && \ +cd /var/modules/unspents && yarn link && \ +cd /var/modules/utxo-core && yarn link && \ cd /var/modules/utxo-ord && yarn link && \ cd /var/modules/account-lib && yarn link && \ cd /var/modules/sdk-coin-ada && yarn link && \ @@ -244,8 +244,6 @@ RUN cd /var/bitgo-express && \ yarn link @bitgo/secp256k1 && \ yarn link @bitgo/sjcl && \ yarn link @bitgo/statics && \ - yarn link @bitgo/utxo-core && \ - yarn link @bitgo/unspents && \ yarn link @bitgo/utxo-lib && \ yarn link @bitgo/blake2b && \ yarn link @bitgo/blake2b-wasm && \ @@ -254,6 +252,8 @@ RUN cd /var/bitgo-express && \ yarn link @bitgo/blockapis && \ yarn link @bitgo/sdk-api && \ yarn link @bitgo/sdk-hmac && \ + yarn link @bitgo/unspents && \ + yarn link @bitgo/utxo-core && \ yarn link @bitgo/utxo-ord && \ yarn link @bitgo/account-lib && \ yarn link @bitgo/sdk-coin-ada && \ diff --git a/modules/sdk-core/package.json b/modules/sdk-core/package.json index bca3ff41ca..0c75d57cce 100644 --- a/modules/sdk-core/package.json +++ b/modules/sdk-core/package.json @@ -45,7 +45,6 @@ "@bitgo/secp256k1": "^1.8.0", "@bitgo/sjcl": "^1.0.1", "@bitgo/statics": "^58.19.0", - "@bitgo/utxo-core": "^1.28.0", "@bitgo/utxo-lib": "^11.19.0", "@noble/curves": "1.8.1", "@stablelib/hex": "^1.0.0", diff --git a/modules/sdk-core/src/bitgo/bip32util.ts b/modules/sdk-core/src/bitgo/bip32util.ts index f3e0d4d6bf..a6462dcc19 100644 --- a/modules/sdk-core/src/bitgo/bip32util.ts +++ b/modules/sdk-core/src/bitgo/bip32util.ts @@ -1,4 +1,4 @@ -import { bip32utils } from '@bitgo/utxo-core'; +import { bip32utils } from '@bitgo/secp256k1'; export const signMessage = bip32utils.signMessage; export const verifyMessage = bip32utils.verifyMessage; diff --git a/modules/secp256k1/package.json b/modules/secp256k1/package.json index 5afee0fced..2ce13b6fd3 100644 --- a/modules/secp256k1/package.json +++ b/modules/secp256k1/package.json @@ -34,6 +34,8 @@ "@brandonblack/musig": "^0.0.1-alpha.0", "@noble/secp256k1": "1.6.3", "bip32": "^3.0.1", + "bitcoinjs-message": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.3", + "bs58check": "^2.1.2", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "ecpair": "npm:@bitgo/ecpair@2.1.0-rc.0" diff --git a/modules/secp256k1/src/bip32utils.ts b/modules/secp256k1/src/bip32utils.ts new file mode 100644 index 0000000000..6772c934d5 --- /dev/null +++ b/modules/secp256k1/src/bip32utils.ts @@ -0,0 +1,69 @@ +import { BIP32Interface } from 'bip32'; +import * as bitcoinMessage from 'bitcoinjs-message'; + +const createHash = require('create-hash'); +const bs58check = require('bs58check'); + +/** + * Computes hash160 (RIPEMD160(SHA256(data))) + */ +function hash160(data: Buffer): Buffer { + const sha256Hash = createHash('sha256').update(data).digest(); + return createHash('ripemd160').update(sha256Hash).digest(); +} + +/** + * Encodes a hash with version byte in Base58Check format + */ +function toBase58Check(hash: Buffer, version: number): string { + const payload = Buffer.allocUnsafe(21); + payload.writeUInt8(version, 0); + hash.copy(payload, 1); + return bs58check.encode(payload); +} + +// Bitcoin mainnet pubKeyHash version byte +const BITCOIN_PUBKEY_HASH_VERSION = 0x00; + +/** + * bip32-aware wrapper around bitcoin-message package + * @see {bitcoinMessage.sign} + */ +export function signMessage( + message: string | Buffer, + privateKey: BIP32Interface | Buffer, + network: { messagePrefix: string } +): Buffer { + if (!Buffer.isBuffer(privateKey)) { + privateKey = privateKey.privateKey as Buffer; + if (!privateKey) { + throw new Error(`must provide privateKey`); + } + } + if (network === null || typeof network !== 'object' || typeof network.messagePrefix !== 'string') { + throw new Error(`invalid argument 'network'`); + } + const compressed = true; + return bitcoinMessage.sign(message, privateKey, compressed, network.messagePrefix); +} + +/** + * bip32-aware wrapper around bitcoin-message package + * @see {bitcoinMessage.verify} + */ +export function verifyMessage( + message: string | Buffer, + publicKey: BIP32Interface | Buffer, + signature: Buffer, + network: { messagePrefix: string } +): boolean { + if (!Buffer.isBuffer(publicKey)) { + publicKey = publicKey.publicKey; + } + if (network === null || typeof network !== 'object' || typeof network.messagePrefix !== 'string') { + throw new Error(`invalid argument 'network'`); + } + + const address = toBase58Check(hash160(publicKey), BITCOIN_PUBKEY_HASH_VERSION); + return bitcoinMessage.verify(message, address, signature, network.messagePrefix); +} diff --git a/modules/secp256k1/src/index.ts b/modules/secp256k1/src/index.ts index 8b8ec66af8..c5133d2429 100644 --- a/modules/secp256k1/src/index.ts +++ b/modules/secp256k1/src/index.ts @@ -197,6 +197,8 @@ const ECPair: ECPairAPI = ECPairFactory(ecc); const bip32: BIP32API = BIP32Factory(ecc); const musig: MuSig = MuSigFactory(crypto); +import * as bip32utils from './bip32utils'; + export { ecc, ECPair, @@ -209,4 +211,5 @@ export { BIP32Interface, musig, MuSig, + bip32utils, }; diff --git a/modules/secp256k1/test/bip32utils.ts b/modules/secp256k1/test/bip32utils.ts new file mode 100644 index 0000000000..deafbe0558 --- /dev/null +++ b/modules/secp256k1/test/bip32utils.ts @@ -0,0 +1,57 @@ +import * as crypto from 'crypto'; +import * as assert from 'assert'; + +import { bip32, bip32utils } from '../src'; + +const { signMessage, verifyMessage } = bip32utils; + +// Bitcoin mainnet message prefix - matches utxolib.networks.bitcoin.messagePrefix +const bitcoinNetwork = { + messagePrefix: '\x18Bitcoin Signed Message:\n', +}; + +describe('bip32utils', function () { + function getSeedBuffers(length: number) { + return Array.from({ length }).map((_, i) => crypto.createHash('sha256').update(`${i}`).digest()); + } + + it('signMessage/verifyMessage', function () { + const keys = getSeedBuffers(4).map((seed) => bip32.fromSeed(seed)); + const messages = ['hello', 'goodbye', Buffer.from('\x01\x02\x03'), Buffer.from('')]; + keys.forEach((key) => { + messages.forEach((message) => { + const signature = signMessage(message, key, bitcoinNetwork); + + keys.forEach((otherKey) => { + messages.forEach((otherMessage) => { + const expectValid = message === otherMessage && key === otherKey; + assert.strictEqual(verifyMessage(otherMessage, otherKey, signature, bitcoinNetwork), expectValid); + assert.strictEqual( + verifyMessage(Buffer.from(otherMessage), otherKey, signature, bitcoinNetwork), + expectValid + ); + }); + }); + }); + }); + }); + + it('signMessage throws on missing privateKey', function () { + const key = bip32.fromSeed(getSeedBuffers(1)[0]); + const neutered = key.neutered(); + assert.throws(() => signMessage('hello', neutered, bitcoinNetwork), /must provide privateKey/); + }); + + it('signMessage throws on invalid network', function () { + const key = bip32.fromSeed(getSeedBuffers(1)[0]); + assert.throws(() => signMessage('hello', key, null as any), /invalid argument 'network'/); + assert.throws(() => signMessage('hello', key, {} as any), /invalid argument 'network'/); + }); + + it('verifyMessage throws on invalid network', function () { + const key = bip32.fromSeed(getSeedBuffers(1)[0]); + const signature = signMessage('hello', key, bitcoinNetwork); + assert.throws(() => verifyMessage('hello', key, signature, null as any), /invalid argument 'network'/); + assert.throws(() => verifyMessage('hello', key, signature, {} as any), /invalid argument 'network'/); + }); +}); diff --git a/modules/utxo-core/package.json b/modules/utxo-core/package.json index 172ffb06ff..9f5fb76d54 100644 --- a/modules/utxo-core/package.json +++ b/modules/utxo-core/package.json @@ -83,7 +83,6 @@ "@bitgo/utxo-lib": "^11.19.0", "@bitgo/wasm-utxo": "1.19.0", "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", - "bitcoinjs-message": "npm:@bitgo-forks/bitcoinjs-message@1.0.0-master.3", "fast-sha256": "^1.3.0" }, "gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c" diff --git a/modules/utxo-core/src/bip32utils.ts b/modules/utxo-core/src/bip32utils.ts index 7ee9aac986..adeed475ca 100644 --- a/modules/utxo-core/src/bip32utils.ts +++ b/modules/utxo-core/src/bip32utils.ts @@ -1,49 +1,13 @@ -import { BIP32Interface } from '@bitgo/secp256k1'; -import * as utxolib from '@bitgo/utxo-lib'; -import * as bitcoinMessage from 'bitcoinjs-message'; +import { bip32utils } from '@bitgo/secp256k1'; + /** - * bip32-aware wrapper around bitcoin-message package - * @see {bitcoinMessage.sign} + * @deprecated Use signMessage from @bitgo/secp256k1 instead + * @see {bip32utils.signMessage} */ -export function signMessage( - message: string | Buffer, - privateKey: BIP32Interface | Buffer, - network: { messagePrefix: string } -): Buffer { - if (!Buffer.isBuffer(privateKey)) { - privateKey = privateKey.privateKey as Buffer; - if (!privateKey) { - throw new Error(`must provide privateKey`); - } - } - if (network === null || typeof network !== 'object' || typeof network.messagePrefix !== 'string') { - throw new Error(`invalid argument 'network'`); - } - const compressed = true; - return bitcoinMessage.sign(message, privateKey, compressed, network.messagePrefix); -} +export const signMessage = bip32utils.signMessage; /** - * bip32-aware wrapper around bitcoin-message package - * @see {bitcoinMessage.verify} + * @deprecated Use verifyMessage from @bitgo/secp256k1 instead + * @see {bip32utils.verifyMessage} */ -export function verifyMessage( - message: string | Buffer, - publicKey: BIP32Interface | Buffer, - signature: Buffer, - network: { messagePrefix: string } -): boolean { - if (!Buffer.isBuffer(publicKey)) { - publicKey = publicKey.publicKey; - } - if (network === null || typeof network !== 'object' || typeof network.messagePrefix !== 'string') { - throw new Error(`invalid argument 'network'`); - } - - const address = utxolib.address.toBase58Check( - utxolib.crypto.hash160(publicKey), - utxolib.networks.bitcoin.pubKeyHash, - utxolib.networks.bitcoin - ); - return bitcoinMessage.verify(message, address, signature, network.messagePrefix); -} +export const verifyMessage = bip32utils.verifyMessage;